Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7783f34998 | ||
|
|
7dcb2fcdd5 | ||
|
|
033a57cad4 | ||
|
|
299228b7b5 | ||
|
|
5b022e2ef3 | ||
|
|
c8818f5774 | ||
|
|
1398857688 | ||
|
|
2a1505bc0d | ||
|
|
4bba6ecc82 | ||
|
|
522c4df44c | ||
|
|
c03b81d2ea | ||
|
|
43a797444a | ||
|
|
d0cb961b49 | ||
|
|
d208e68885 | ||
|
|
8dcb53aea0 | ||
|
|
241b286e21 | ||
|
|
17839f6a25 | ||
|
|
e4d3faefa0 | ||
|
|
116ca7f011 | ||
|
|
d44339b018 | ||
|
|
352fd23c0b | ||
|
|
65b8868c36 | ||
|
|
54dec171f9 | ||
|
|
5b4398911a | ||
|
|
4c0bd132b1 | ||
|
|
7ce4eddb0e | ||
|
|
14519135d8 | ||
|
|
60839481ad | ||
|
|
3ef1941fc0 | ||
|
|
16cb1a8026 | ||
|
|
e3b9bd6a70 | ||
|
|
a1ea04acf8 | ||
|
|
3e341a3555 | ||
|
|
cb5c9b03bf | ||
|
|
884a1b9d6f | ||
|
|
e8aa498683 | ||
|
|
1b7efd854a | ||
|
|
8b32c3dcb6 | ||
|
|
0821b28eac | ||
|
|
4c71d5ac42 | ||
|
|
a8e0b5a5ac | ||
|
|
9b9014a042 | ||
|
|
96b42b1784 | ||
|
|
26d854ebe8 | ||
|
|
ed36d01b40 | ||
|
|
646556a457 | ||
|
|
d20a35eddc | ||
|
|
d69946db8a | ||
|
|
ddab4075d2 | ||
|
|
d6c8ad1d0d | ||
|
|
54c6faa4b6 | ||
|
|
c88f05c5c0 | ||
|
|
e419c74ebb | ||
|
|
b6dc66b070 | ||
|
|
7b4d9bf5db | ||
|
|
c51e7351e8 | ||
|
|
fe12eccc42 | ||
|
|
17b7826753 | ||
|
|
14ae6d8b1a | ||
|
|
7b559991c7 | ||
|
|
decd4cdf0a | ||
|
|
dc7a6551c5 | ||
|
|
526467747e | ||
|
|
b8d295a5b7 | ||
|
|
293b6267be | ||
|
|
20e70854a1 | ||
|
|
11173e6460 | ||
|
|
069a7922fc | ||
|
|
98338ffb00 | ||
|
|
62dec118af | ||
|
|
84b406534d | ||
|
|
d9d2d59b73 | ||
|
|
20d4b954c0 | ||
|
|
5edb845d8f | ||
|
|
cfe2b43d2a | ||
|
|
a0ac41a9ce | ||
|
|
194a527736 | ||
|
|
6f135c0696 | ||
|
|
9a767b627f | ||
|
|
c1c4eda69d | ||
|
|
6c46867698 | ||
|
|
a73eb569ae | ||
|
|
f07b40735e | ||
|
|
94f74d3f56 | ||
|
|
d9a5e63e51 | ||
|
|
c8e5c23ac2 | ||
|
|
59968e38d1 | ||
|
|
4f45781319 | ||
|
|
22ea83e843 | ||
|
|
55fed49be1 | ||
|
|
311d9a668b | ||
|
|
165f5093f4 | ||
|
|
43d92ddb51 | ||
|
|
10944bf100 | ||
|
|
ce9b2c3b77 | ||
|
|
4c102a08e8 | ||
|
|
1d2b939e06 | ||
|
|
e1bb2113ed | ||
|
|
b71113700a | ||
|
|
ebfcc46c9e | ||
|
|
c047ae6808 | ||
|
|
023a1a088e | ||
|
|
8eabf53642 | ||
|
|
824a8fa7c8 | ||
|
|
a3b7c76523 | ||
|
|
26ca8c71d7 | ||
|
|
0d2447faf3 | ||
|
|
4cea06c5a5 | ||
|
|
031ce14857 | ||
|
|
bd143f5c14 | ||
|
|
4e9c992a91 | ||
|
|
d6ac3d2672 | ||
|
|
1933d896cb | ||
|
|
168a3bcddb | ||
|
|
adc342ab9a | ||
|
|
bbf3cd96f1 | ||
|
|
e8c9304519 | ||
|
|
815c3e61e4 | ||
|
|
4e55ea18e5 | ||
|
|
81b70acf5f | ||
|
|
165d7499ea | ||
|
|
abbcfbeb6a | ||
|
|
bff44b7097 | ||
|
|
95f6ae2bb6 | ||
|
|
a15af87f9b | ||
|
|
97f8a4ab38 | ||
|
|
7b9d370452 | ||
|
|
db72953310 | ||
|
|
bb918a8523 | ||
|
|
24b02cc656 | ||
|
|
9877ca602f | ||
|
|
c9093715b7 | ||
|
|
e76ba62b46 | ||
|
|
2d00f4c41c | ||
|
|
3ad18ca365 | ||
|
|
cf4d575062 | ||
|
|
9f835f8350 | ||
|
|
297c42371d | ||
|
|
8352223704 | ||
|
|
f9c9c52d75 | ||
|
|
7704029e59 | ||
|
|
a83fb13335 | ||
|
|
b8e6e80bdf | ||
|
|
ba90bc5fea | ||
|
|
d7257af7a0 | ||
|
|
f36771db9d | ||
|
|
aa68d67b5e | ||
|
|
5fc472a8d9 | ||
|
|
fa49133bd7 | ||
|
|
47bd96246f | ||
|
|
cae39bc19f | ||
|
|
4839cdf2f8 | ||
|
|
08571368c0 | ||
|
|
9fcf566540 | ||
|
|
5f36800100 | ||
|
|
e46b12d267 | ||
|
|
62b0e3fe89 | ||
|
|
992e3bfb9a | ||
|
|
43b8c744e6 | ||
|
|
d0107f7830 | ||
|
|
b66396857b | ||
|
|
9c85e1156c | ||
|
|
3b0024bcf2 | ||
|
|
4c4f0922a4 | ||
|
|
8e63756c61 | ||
|
|
0eb74c86ba | ||
|
|
e65ec56276 | ||
|
|
90c54f2c30 | ||
|
|
6a27b91fcb | ||
|
|
4dcf26bcb3 | ||
|
|
d5ae1e3d6a | ||
|
|
6f0d911a08 | ||
|
|
3e91200d3f | ||
|
|
f989eba2c9 | ||
|
|
11d57e9d52 | ||
|
|
62fa22098e | ||
|
|
4417d53562 | ||
|
|
b380108a4f | ||
|
|
87a42087de | ||
|
|
bee857c2af | ||
|
|
31782040c6 | ||
|
|
1195dd3eb3 | ||
|
|
a61d74deff | ||
|
|
d1b89bc68a | ||
|
|
5f8faf237c | ||
|
|
5b62f1c586 | ||
|
|
7306df3445 | ||
|
|
10fb35b47f | ||
|
|
0ffba99eaf | ||
|
|
18ebe010bc | ||
|
|
4817a62a66 | ||
|
|
5473d9e940 | ||
|
|
1ab68e80b3 | ||
|
|
3e142fab0f | ||
|
|
460ceeba1d | ||
|
|
14e678f661 | ||
|
|
a2469abebd | ||
|
|
3b01af242c | ||
|
|
4d1c4f6348 | ||
|
|
fed2d41139 | ||
|
|
c8147bd83f | ||
|
|
29e779d09d | ||
|
|
3a134672e9 | ||
|
|
6b52c42b2b | ||
|
|
811292529b | ||
|
|
5262f56aaf | ||
|
|
ddd27fe426 | ||
|
|
c15beaa014 | ||
|
|
1c38b129c0 |
+163
-80
@@ -1,82 +1,17 @@
|
||||
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
|
||||
|
||||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
######################################
|
||||
########### Admin Settings ###########
|
||||
######################################
|
||||
|
||||
# uvicorn variable, uncomment to allow https behind a proxy
|
||||
# FORWARDED_ALLOW_IPS="*"
|
||||
|
||||
DEBUG=false
|
||||
|
||||
# logging into LNBITS_DATA_FOLDER/logs/
|
||||
ENABLE_LOG_TO_FILE=true
|
||||
|
||||
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
||||
LOG_ROTATION="100 MB"
|
||||
LOG_RETENTION="3 months"
|
||||
|
||||
# Server security, rate limiting ips, blocked ips, allowed ips
|
||||
LNBITS_RATE_LIMIT_NO="200"
|
||||
LNBITS_RATE_LIMIT_UNIT="minute"
|
||||
LNBITS_ALLOWED_IPS=""
|
||||
LNBITS_BLOCKED_IPS=""
|
||||
|
||||
# Allow users and admins by user IDs (comma separated list)
|
||||
# if set new users will not be able to create accounts
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
|
||||
# Disable account creation for new users
|
||||
# LNBITS_ALLOW_NEW_ACCOUNTS=false
|
||||
|
||||
# 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.
|
||||
# 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
|
||||
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
|
||||
# Ad space description
|
||||
# LNBITS_AD_SPACE_TITLE="Supported by"
|
||||
# csv ad space, format "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", extensions can choose to honor
|
||||
# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png"
|
||||
|
||||
# Hides wallet api, extensions can choose to honor
|
||||
LNBITS_HIDE_API=false
|
||||
|
||||
# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json"
|
||||
# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN
|
||||
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Path where extensions will be installed (defaults to `./lnbits/`).
|
||||
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
||||
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
||||
|
||||
|
||||
# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart.
|
||||
# The extension must be removed from this list in order to not be re-installed.
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
||||
|
||||
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
||||
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
||||
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
||||
# for both PostgreSQL and CockroachDB, you'll need to install
|
||||
# psycopg2 as an additional dependency
|
||||
LNBITS_DATA_FOLDER="./data"
|
||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
||||
|
||||
LNBITS_SERVICE_FEE="0.0"
|
||||
# value in millisats
|
||||
LNBITS_RESERVE_FEE_MIN=2000
|
||||
# value in percent
|
||||
LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
|
||||
# Limit fiat currencies allowed to see in UI
|
||||
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
@@ -85,8 +20,15 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
|
||||
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 ############
|
||||
######################################
|
||||
|
||||
# which fundingsources are allowed in the admin ui
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, OpenNodeWallet"
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, OpenNodeWallet"
|
||||
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
@@ -109,8 +51,7 @@ CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||
|
||||
# CoreLightningRestWallet
|
||||
CORELIGHTNING_REST_URL=http://127.0.0.1:8185/
|
||||
# Path or BASE64/HEX STRING
|
||||
CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon"
|
||||
CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" # or BASE64/HEXSTRING
|
||||
CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert"
|
||||
|
||||
# LnbitsWallet
|
||||
@@ -120,15 +61,15 @@ LNBITS_KEY=LNBITS_ADMIN_KEY
|
||||
# LndWallet
|
||||
LND_GRPC_ENDPOINT=127.0.0.1
|
||||
LND_GRPC_PORT=10009
|
||||
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
|
||||
LND_GRPC_CERT="/home/bob/.lnd/tls.cert"
|
||||
LND_GRPC_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING
|
||||
# To use an AES-encrypted macaroon, set
|
||||
# LND_GRPC_MACAROON="eNcRyPtEdMaCaRoOn"
|
||||
|
||||
# LndRestWallet
|
||||
LND_REST_ENDPOINT=https://127.0.0.1:8080/
|
||||
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
|
||||
LND_REST_CERT="/home/bob/.lnd/tls.cert"
|
||||
LND_REST_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING
|
||||
# To use an AES-encrypted macaroon, set
|
||||
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
|
||||
|
||||
@@ -139,6 +80,14 @@ LNPAY_API_KEY=LNPAY_API_KEY
|
||||
# Wallet Admin in Wallet Access Keys
|
||||
LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
|
||||
|
||||
# AlbyWallet
|
||||
ALBY_API_ENDPOINT=https://api.getalby.com/
|
||||
ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
|
||||
|
||||
# ZBDWallet
|
||||
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
|
||||
ZBD_API_KEY=ZBD_ACCESS_TOKEN
|
||||
|
||||
# OpenNodeWallet
|
||||
OPENNODE_API_ENDPOINT=https://api.opennode.com/
|
||||
OPENNODE_KEY=OPENNODE_ADMIN_KEY
|
||||
@@ -155,3 +104,137 @@ ECLAIR_PASS=eclairpw
|
||||
# Enter /api in LightningTipBot to get your key
|
||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||
LNTIPS_API_ENDPOINT=https://ln.tips
|
||||
|
||||
######################################
|
||||
####### 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, 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 that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
|
||||
GOOGLE_CLIENT_ID=""
|
||||
GOOGLE_CLIENT_SECRET=""
|
||||
|
||||
# GitHub OAuth Config
|
||||
# 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
|
||||
# FORWARDED_ALLOW_IPS="*"
|
||||
|
||||
# Server security, rate limiting ips, blocked ips, allowed ips
|
||||
LNBITS_RATE_LIMIT_NO="200"
|
||||
LNBITS_RATE_LIMIT_UNIT="minute"
|
||||
LNBITS_ALLOWED_IPS=""
|
||||
LNBITS_BLOCKED_IPS=""
|
||||
|
||||
# Allow users and admins by user IDs (comma separated list)
|
||||
# if set new users will not be able to create accounts
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# ID of the super user. The user ID must exist.
|
||||
# SUPER_USER=""
|
||||
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
|
||||
# Start LNbits core only. The extensions are not loaded.
|
||||
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
|
||||
|
||||
# Disable account creation for new users
|
||||
# LNBITS_ALLOW_NEW_ACCOUNTS=false
|
||||
|
||||
# Enable Node Management without activating the LNBITS Admin GUI
|
||||
# by setting the following variables to true.
|
||||
LNBITS_NODE_UI=false
|
||||
LNBITS_PUBLIC_NODE_UI=false
|
||||
# Enabling the transactions tab can cause crashes on large Core Lightning nodes.
|
||||
LNBITS_NODE_UI_TRANSACTIONS=false
|
||||
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
|
||||
# Ad space description
|
||||
# LNBITS_AD_SPACE_TITLE="Supported by"
|
||||
# csv ad space, format "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", extensions can choose to honor
|
||||
# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png"
|
||||
|
||||
# Hides wallet api, extensions can choose to honor
|
||||
LNBITS_HIDE_API=false
|
||||
|
||||
# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json"
|
||||
# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN
|
||||
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Path where extensions will be installed (defaults to `./lnbits/`).
|
||||
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
||||
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
||||
|
||||
# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart.
|
||||
# The extension must be removed from this list in order to not be re-installed.
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
||||
|
||||
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
||||
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
||||
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
||||
# for both PostgreSQL and CockroachDB, you'll need to install
|
||||
# psycopg2 as an additional dependency
|
||||
LNBITS_DATA_FOLDER="./data"
|
||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
||||
|
||||
# the service fee (in percent)
|
||||
LNBITS_SERVICE_FEE=0.0
|
||||
# the wallet where fees go to
|
||||
# LNBITS_SERVICE_FEE_WALLET=
|
||||
# the maximum fee per transaction (in satoshis)
|
||||
# LNBITS_SERVICE_FEE_MAX=1000
|
||||
# disable fees for internal transactions
|
||||
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
|
||||
|
||||
# value in millisats
|
||||
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"
|
||||
|
||||
######################################
|
||||
###### Logging and Development #######
|
||||
######################################
|
||||
|
||||
DEBUG=false
|
||||
DEBUG_DATABASE=false
|
||||
BUNDLE_ASSETS=true
|
||||
|
||||
# logging into LNBITS_DATA_FOLDER/logs/
|
||||
ENABLE_LOG_TO_FILE=true
|
||||
|
||||
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
||||
LOG_ROTATION="100 MB"
|
||||
LOG_RETENTION="3 months"
|
||||
|
||||
# for database cleanup commands
|
||||
# CLEANUP_WALLETS_DAYS=90
|
||||
|
||||
@@ -7,7 +7,7 @@ inputs:
|
||||
default: "3.9"
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
default: "1.5.1"
|
||||
default: "1.7.0"
|
||||
node-version:
|
||||
description: "Node Version"
|
||||
default: "18.x"
|
||||
|
||||
@@ -38,9 +38,6 @@ jobs:
|
||||
with:
|
||||
make: openapi
|
||||
|
||||
# docker:
|
||||
# uses: ./.github/workflows/docker.yml
|
||||
|
||||
regtest:
|
||||
needs: [ lint ]
|
||||
uses: ./.github/workflows/regtest.yml
|
||||
@@ -51,3 +48,13 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
||||
|
||||
jmeter:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.5.1"]
|
||||
uses: ./.github/workflows/jmeter.yml
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
|
||||
@@ -1,20 +1,58 @@
|
||||
name: docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
default: latest
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
default: latest
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
push_to_dockerhub:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: lnbitsdocker/lnbits-legend:latest
|
||||
cache-from: type=registry,ref=lnbitsdocker/lnbits-legend:latest
|
||||
cache-to: type=inline
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:${{ inputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Build and push latest tag
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
name: JMeter Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
python-version:
|
||||
description: "Python Version"
|
||||
required: true
|
||||
default: "3.9"
|
||||
type: string
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
required: true
|
||||
default: "1.5.1"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
action_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
cache: "pip"
|
||||
|
||||
- name: Set up Poetry ${{ inputs.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ inputs.poetry-version }}
|
||||
|
||||
- name: create logs and reports dir
|
||||
run: |
|
||||
mkdir logs
|
||||
mkdir reports
|
||||
|
||||
- name: install packages
|
||||
run: poetry install
|
||||
|
||||
- name: run LNbits
|
||||
env:
|
||||
LNBITS_ADMIN_UI: true
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
run: |
|
||||
poetry run lnbits > logs/lnbits.log &
|
||||
sleep 5
|
||||
|
||||
|
||||
- name: install jmeter
|
||||
run: |
|
||||
java -version
|
||||
wget https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.6.2.zip
|
||||
unzip apache-jmeter-5.6.2.zip
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -v
|
||||
|
||||
- name: start mirror server
|
||||
run: |
|
||||
libs_dir=$GITHUB_WORKSPACE/apache-jmeter-5.6.2/lib/
|
||||
echo "Fix bad Jmeter lib names. Lib dir: $libs_dir"
|
||||
|
||||
mv $libs_dir/slf4j-api-1.7.36.jar $libs_dir/slf4j-api-1.7.25.jar
|
||||
mv $libs_dir/log4j-slf4j-impl-2.20.0.jar $libs_dir/log4j-slf4j-impl-2.11.0.jar
|
||||
mv $libs_dir/log4j-api-2.20.0.jar $libs_dir/log4j-api-2.11.1.jar
|
||||
mv $libs_dir/log4j-core-2.20.0.jar $libs_dir/log4j-core-2.11.1.jar
|
||||
mv $libs_dir/log4j-1.2-api-2.20.0.jar $libs_dir/og4j-1.2-api-2.11.1.jar
|
||||
|
||||
echo "Starting the mirror server on dfault port 8081."
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/mirror-server &
|
||||
|
||||
- name: run jmx scripts
|
||||
run: |
|
||||
for file in $( ls $GITHUB_WORKSPACE/integration/*.jmx); do
|
||||
echo "Running test with $file"
|
||||
filename=$(basename "$file" ".jmx")
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -n -t $file -l logs/$filename.log -e -o reports ;
|
||||
error_count=$(cat jmeter.log | grep "summary =" | awk '{print $19}')
|
||||
echo "Error count: $error_count"
|
||||
if [[ "$error_count" == "0" ]]; then
|
||||
echo "Test $filename OK."
|
||||
rm -r reports/*
|
||||
else
|
||||
echo "Test $filename failed. Error count: $error_count."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: jmeter-test-results
|
||||
path: |
|
||||
reports/
|
||||
logs/
|
||||
|
||||
@@ -3,25 +3,53 @@ on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
|
||||
black:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: checkblack
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
ruff:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: checkruff
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
mypy:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: mypy
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
pyright:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: pyright
|
||||
python-version: ${{ matrix.python-version }}
|
||||
npm: true
|
||||
|
||||
|
||||
prettier:
|
||||
uses: ./.github/workflows/make.yml
|
||||
with:
|
||||
make: checkprettier
|
||||
npm: true
|
||||
|
||||
bundle:
|
||||
uses: ./.github/workflows/make.yml
|
||||
with:
|
||||
make: checkbundle
|
||||
npm: true
|
||||
|
||||
@@ -11,21 +11,24 @@ on:
|
||||
description: "use npm install"
|
||||
default: false
|
||||
type: boolean
|
||||
python-version:
|
||||
description: "python version"
|
||||
type: string
|
||||
default: "3.10"
|
||||
|
||||
jobs:
|
||||
make:
|
||||
name: ${{ inputs.make }} (${{ matrix.python-version }})
|
||||
name: ${{ inputs.make }} (${{ inputs.python-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
os-version: ["ubuntu-latest"]
|
||||
python-version: ["3.9", "3.10"]
|
||||
node-version: ["18.x"]
|
||||
runs-on: ${{ matrix.os-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
python-version: ${{ inputs.python-version }}
|
||||
node-version: ${{ matrix.node-version }}
|
||||
npm: ${{ inputs.npm }}
|
||||
- run: make ${{ inputs.make }}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
name: LNbits CI / nix
|
||||
|
||||
# - run : on main, dev, nix and cachix branches when relevant files change
|
||||
# - cache : on main, dev and cachix branches when relevant files change
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- nix
|
||||
- cachix
|
||||
paths:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
- '.github/workflows/nix.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'flake.nix'
|
||||
@@ -21,8 +27,15 @@ jobs:
|
||||
nix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v23
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v24
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- run: nix build
|
||||
nix_path: nixpkgs=channel:nixos-23.11
|
||||
- uses: cachix/cachix-action@v13
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/cachix'
|
||||
with:
|
||||
name: lnbits
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
- run: nix build -L
|
||||
- run: cachix push lnbits ./result
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/cachix'
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
name: Build and push Docker image on tag
|
||||
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+-*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
name: Build and push lnbits image
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Import environment variables
|
||||
id: import-env
|
||||
shell: bash
|
||||
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
- name: Show set environment variables
|
||||
run: |
|
||||
printf " TAG: %s\n" "$TAG"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Show available Docker buildx platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Run Docker buildx against tag
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:${TAG} \
|
||||
--output "type=registry" ./
|
||||
|
||||
- name: Run Docker buildx against latest
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \
|
||||
--output "type=registry" ./
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Create github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$tag" --generate-notes --draft
|
||||
|
||||
docker:
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
@@ -35,6 +35,8 @@ coverage.xml
|
||||
node_modules
|
||||
lnbits/static/bundle.js
|
||||
lnbits/static/bundle.css
|
||||
lnbits/static/bundle.min.js.old
|
||||
lnbits/static/bundle.min.css.old
|
||||
docker
|
||||
|
||||
# Nix
|
||||
|
||||
@@ -14,11 +14,11 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
- id: check-case-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.7.0
|
||||
rev: 24.2.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.283
|
||||
rev: v0.3.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ FROM python:3.10-slim-bullseye
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl pkg-config build-essential
|
||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
@@ -4,7 +4,7 @@ all: format check
|
||||
|
||||
format: prettier black ruff
|
||||
|
||||
check: mypy pyright checkblack checkruff checkprettier
|
||||
check: mypy pyright checkblack checkruff checkprettier checkbundle
|
||||
|
||||
prettier:
|
||||
poetry run ./node_modules/.bin/prettier --write lnbits
|
||||
@@ -96,9 +96,16 @@ bundle:
|
||||
npm run vendor_minify_css
|
||||
npm run vendor_bundle_js
|
||||
npm run vendor_minify_js
|
||||
# increment serviceworker version
|
||||
sed -i -e "s/CACHE_VERSION =.*/CACHE_VERSION = $$(awk '/CACHE_VERSION =/ { print 1+$$4 }' lnbits/core/static/js/service-worker.js)/" \
|
||||
lnbits/core/static/js/service-worker.js
|
||||
|
||||
checkbundle:
|
||||
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
|
||||
cp lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old
|
||||
make bundle
|
||||
diff -q lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old || exit 1
|
||||
diff -q lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old || exit 1
|
||||
@echo "Bundle is OK"
|
||||
rm lnbits/static/bundle.min.js.old
|
||||
rm lnbits/static/bundle.min.css.old
|
||||
|
||||
install-pre-commit-hook:
|
||||
@echo "Installing pre-commit hook to git"
|
||||
|
||||
@@ -21,9 +21,18 @@ LNbits is a Python server that sits on top of any funding source. It can be used
|
||||
- Fallback wallet for the LNURL scheme
|
||||
- Instant wallet for LN demonstrations
|
||||
|
||||
LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, LightningTipBot, and with more being added regularly.
|
||||
LNbits can run on top of any Lightning funding source. It currently supports the following, but more and more are added regularly:
|
||||
- LND (REST and gRPC)
|
||||
- Core Lightning aka CLN (gRPC, REST and Spark)
|
||||
- Eclair
|
||||
- LNPay
|
||||
- LNbits
|
||||
- OpenNode
|
||||
- Alby
|
||||
- ZBD
|
||||
- LightningTipBot
|
||||
|
||||
See [LNbits Wiki](https://github.com/lnbits/lnbits/wiki/) for more detailed documentation.
|
||||
See [LNbits manual](https://docs.lnbits.org/guide/wallets.html) for more detailed documentation about each funding source.
|
||||
|
||||
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
||||
|
||||
|
||||
+1
-1
@@ -9,4 +9,4 @@ nav_order: 3
|
||||
API reference
|
||||
=============
|
||||
|
||||
[Swagger Docs](https://docs.lnbits.org/devs/swagger.html)
|
||||
[Swagger Docs](https://legend.lnbits.com/docs)
|
||||
|
||||
@@ -21,7 +21,7 @@ mv templates/example templates/mysuperplugin # Rename templates folder.
|
||||
- if you are on macOS and having difficulty with 'sed', consider `brew install gnu-sed` and use 'gsed', without -0 option after xargs.
|
||||
|
||||
1. Edit `manifest.json` and change the organisation name to your GitHub username.
|
||||
1. Push your changes to GitHub.
|
||||
1. Push your changes to GitHub.
|
||||
1. In GitHub create a new release for your extension repo. Tag the release with `0.0.1`
|
||||
1. Copy the URL of the extension's raw `manifest.json` URL `https://raw.githubusercontent.com/[my-user-name]/mysuperplugin/master/manifest.json`
|
||||
1. If you are using the LMNbits Admin UI, go to the Admin UI > Server > Extension Sources, click "Add", paste the URL, then click "Save"
|
||||
|
||||
@@ -60,7 +60,7 @@ $ poetry run lnbits
|
||||
```
|
||||
You can now `cat` the Super User ID:
|
||||
```
|
||||
$ cat .super_user
|
||||
$ cat data/.super_user
|
||||
123de4bfdddddbbeb48c8bc8382fe123
|
||||
```
|
||||
You can access your super user account at `/wallet?usr=super_user_id`. You just have to append it to your normal LNbits web domain.
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@ allow-self-payment=1
|
||||
|
||||
<details><summary>Which funding sources can I use for LNbits?</summary>
|
||||
<p>There are several ways to run a LNbits instance funded from different sources. It is important to choose a source that has a good liquidity and good peers connected. If you use LNbits for public services your users´ payments can then flow happily in both directions. If you would like to fund your LNbits wallet via btc please see section Troubleshooting.</p>
|
||||
<p>The <a href="http://docs.lnbits.org/guide/wallets.html">LNbits manual</a> shows you which sources can be used and how to configure each: CLN, LND, LNPay, Cliche, OpenNode as well as bots.</p>
|
||||
<p>The <a href="https://docs.lnbits.org/guide/wallets.html">LNbits manual</a> shows you which sources can be used and how to configure each.</p>
|
||||
</details>
|
||||
|
||||
<!--Later to be added
|
||||
@@ -47,7 +47,7 @@ allow-self-payment=1
|
||||
<p>When you run your LNbits in clearnet basically everyone can generate a wallet on it. Since the funds of your node are bound to these wallets you might want to prevent that. There are two ways to do so:</p>
|
||||
<ul>
|
||||
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/lnbits/blob/main/.env.example">in the .env file</a></li>
|
||||
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/usermanager">via the Usermanager-Extension</a>. You can find <a href="http://docs.lnbits.org/guide/admin_ui.html">more info about the superuser and the admin extension here</a></li>
|
||||
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/usermanager">via the Usermanager-Extension</a>. You can find <a href="https://docs.lnbits.org/guide/admin_ui.html">more info about the superuser and the admin extension here</a></li>
|
||||
</ul>
|
||||
<p>Please note that all entries in the .env file will not be the taken into account once you activated the admin extension.</p>
|
||||
</details>
|
||||
@@ -263,6 +263,6 @@ allow-self-payment=1
|
||||
|
||||
## Developing for LNbits
|
||||
<ul>
|
||||
<li><a href="http://docs.lnbits.org/devs/development.html">Making Estension / how to use Websockets / API reference</a></li>
|
||||
<li><a href="https://docs.lnbits.org/devs/development.html">Making extensions / How to use Websockets / API reference</a></li>
|
||||
<li><a href="https://t.me/lnbits">Telegram LNbits Support Group</a></li></ul>
|
||||
</ul>
|
||||
|
||||
+28
-13
@@ -16,8 +16,8 @@ Mininum poetry version has is ^1.2, but it is recommended to use latest poetry.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
git checkout main
|
||||
cd lnbits
|
||||
git checkout main
|
||||
|
||||
# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version
|
||||
sudo apt update
|
||||
@@ -61,44 +61,59 @@ poetry install --only main
|
||||
|
||||
## Option 2: Nix
|
||||
|
||||
> note: currently not supported while we make some architectural changes on the path to leave beta
|
||||
|
||||
```sh
|
||||
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
|
||||
sh <(curl -L https://nixos.org/nix/install) --daemon
|
||||
|
||||
# Enable nix-command and flakes experimental features for nix:
|
||||
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
|
||||
|
||||
# Add cachix for cached binaries
|
||||
nix-env -iA cachix -f https://cachix.org/api/v1/install
|
||||
cachix use lnbits
|
||||
|
||||
# Clone and build LNbits
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
# Modern debian distros usually include Nix, however you can install with:
|
||||
# 'sh <(curl -L https://nixos.org/nix/install) --daemon', or use setup here https://nixos.org/download.html#nix-verify-installation
|
||||
nix build
|
||||
|
||||
nix build .#lnbits
|
||||
mkdir data
|
||||
|
||||
```
|
||||
|
||||
#### Running the server
|
||||
|
||||
```sh
|
||||
# .env variables are currently passed when running
|
||||
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
|
||||
nix run
|
||||
```
|
||||
|
||||
Ideally you would set the environment via the `.env` file,
|
||||
but you can also set the env variables or pass command line arguments:
|
||||
|
||||
``` sh
|
||||
# .env variables are currently passed when running, but LNbits can be managed with the admin UI.
|
||||
LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
|
||||
|
||||
# Once you have created a user, you can set as the super_user
|
||||
SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
|
||||
```
|
||||
|
||||
## Option 3: Docker
|
||||
|
||||
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
|
||||
|
||||
+17
-2
@@ -8,7 +8,7 @@ nav_order: 3
|
||||
Backend wallets
|
||||
===============
|
||||
|
||||
LNbits can run on top of many lightning-network funding sources. Currently there is support for CoreLightning, LND, LNbits, LNPay and OpenNode, with more being added regularly.
|
||||
LNbits can run on top of many lightning-network funding sources with more being added regularly.
|
||||
|
||||
A backend wallet can be configured using the following LNbits environment variables:
|
||||
|
||||
@@ -19,6 +19,7 @@ A backend wallet can be configured using the following LNbits environment variab
|
||||
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
|
||||
### CoreLightning REST
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningRestWallet**
|
||||
- `CORELIGHTNING_REST_URL`: http://127.0.0.1:8185/
|
||||
- `CORELIGHTNING_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
|
||||
@@ -70,7 +71,6 @@ For the invoice listener to work you have a publicly accessible URL in your LNbi
|
||||
- `LNPAY_API_KEY`: sak_apiKey
|
||||
- `LNPAY_WALLET_KEY`: waka_apiKey
|
||||
|
||||
|
||||
### OpenNode
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary.
|
||||
@@ -79,6 +79,21 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||
|
||||
### Alby
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an alby access token here: https://getalby.com/developer/access_tokens/new
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **AlbyWallet**
|
||||
- `ALBY_API_ENDPOINT`: https://api.getalby.com/
|
||||
- `ALBY_ACCESS_TOKEN`: AlbyAccessToken
|
||||
|
||||
### ZBD
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an ZBD API Key here: https://zbd.dev/docs/dashboard/projects/api
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **ZBDWallet**
|
||||
- `ZBD_API_ENDPOINT`: https://api.zebedee.io/v0/
|
||||
- `ZBD_API_KEY`: ZBDApiKey
|
||||
|
||||
### Cliche Wallet
|
||||
|
||||
|
||||
Generated
+51
-14
@@ -5,11 +5,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -26,11 +26,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688870561,
|
||||
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
|
||||
"lastModified": 1698974481,
|
||||
"narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
|
||||
"rev": "4bb5e752616262457bc7ca5882192a564c0472d2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -41,16 +41,16 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1678470307,
|
||||
"narHash": "sha256-OEeMUr3ueLIXyW/OaFUX5jUdimyQwMg/7e+/Q0gC/QE=",
|
||||
"lastModified": 1702233072,
|
||||
"narHash": "sha256-H5G2wgbim2Ku6G6w+NSaQaauv6B6DlPhY9fMvArKqRo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0c4800d579af4ed98ecc47d464a5e7b0870c4b1f",
|
||||
"rev": "781e2a9797ecf0f146e81425c822dca69fe4a348",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -61,14 +61,16 @@
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
],
|
||||
"systems": "systems_2",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1695386222,
|
||||
"narHash": "sha256-5lgnhCCGW0NH5+m5iTED8u6NSSM/dbH9LBPvX0x0XXg=",
|
||||
"lastModified": 1702334837,
|
||||
"narHash": "sha256-QZG6+zFshyY+L8m2tlOTm75U5m9y7z01g0josVK+8Os=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "093383b3d7fdd36846a7d84e128ca11865800538",
|
||||
"rev": "1f4bcbf1be73abc232a972a77102a3e820485a99",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -97,6 +99,41 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "systems",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1699786194,
|
||||
"narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
|
||||
poetry2nix = {
|
||||
url = "github:nix-community/poetry2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
@@ -13,19 +13,11 @@
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
forSystems = systems: f:
|
||||
nixpkgs.lib.genAttrs systems
|
||||
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
|
||||
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlays.default self.overlays.default ]; }));
|
||||
forAllSystems = forSystems supportedSystems;
|
||||
projectName = "lnbits";
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (system: pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
nodePackages.prettier
|
||||
poetry
|
||||
];
|
||||
};
|
||||
});
|
||||
overlays = {
|
||||
default = final: prev: {
|
||||
${projectName} = self.packages.${prev.stdenv.hostPlatform.system}.${projectName};
|
||||
@@ -36,20 +28,11 @@
|
||||
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
|
||||
projectDir = ./.;
|
||||
meta.rev = self.dirtyRev or self.rev;
|
||||
meta.mainProgram = projectName;
|
||||
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: {
|
||||
protobuf = prev.protobuf.override { preferWheel = true; };
|
||||
ruff = prev.ruff.override { preferWheel = true; };
|
||||
fastapi = prev.fastapi.overridePythonAttrs (old: {
|
||||
postPatch = ''
|
||||
substituteInPlace pyproject.toml \
|
||||
--replace '"Framework :: Pydantic",' "" \
|
||||
--replace '"Framework :: Pydantic :: 1",' ""
|
||||
'';
|
||||
});
|
||||
bolt11 = prev.bolt11.overrideAttrs (old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
||||
prev.poetry
|
||||
];
|
||||
});
|
||||
wallycore = prev.wallycore.override { preferWheel = true; };
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
|
||||
<hashTree>
|
||||
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
|
||||
<hashTree>
|
||||
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Account" enabled="true"/>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get main page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Create new account" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"name":"a1"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/account</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract user and wallet" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
|
||||
|
||||
vars.put("userId", resp.user || "no-user-id");
|
||||
vars.put("walletId", resp.id || "no-wallet-id");
|
||||
vars.put("inkey", resp.inkey || 'no-inkey');
|
||||
vars.put("adminkey", resp.adminkey || 'no-adminkey');
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Login with user id" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"usr":"${userId}"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/usr</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Go to wallet page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="wal" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.name">wal</stringProp>
|
||||
<stringProp name="Argument.value">${walletId}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/wallet</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get currencies" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/currencies</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">undefined</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Currency List" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="1438956759">["AED","AFN","ALL","AMD","ANG","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTN","BWP","BYN","BYR","BZD","CAD","CDF","CHF","CLF","CLP","CNH","CNY","COP","CRC","CUC","CVE","CZK","DJF","DKK","DOP","DZD","EGP","ERN","ETB","EUR","FJD","FKP","GBP","GEL","GGP","GHS","GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF","IDR","ILS","IMP","INR","IQD","IRT","ISK","JEP","JMD","JOD","JPY","KES","KGS","KHR","KMF","KRW","KWD","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LYD","MAD","MDL","MGA","MKD","MMK","MNT","MOP","MRO","MUR","MVR","MWK","MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PAB","PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB","RWF","SAR","SBD","SCR","SEK","SGD","SHP","SLL","SOS","SRD","SSP","STD","SVC","SZL","THB","TJS","TMT","TND","TOP","TRY","TTD","TWD","TZS","UAH","UGX","USD","UYU","UZS","VEF","VES","VND","VUV","WST","XAF","XAG","XAU","XCD","XDR","XOF","XPD","XPF","XPT","YER","ZAR","ZMW","ZWL"]</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get wallet balance" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/wallet</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">${inkey}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223Assertion guiclass="TestBeanGUI" testclass="JSR223Assertion" testname="Check Balance is Zero" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
|
||||
|
||||
if (resp.balance !== 0) {
|
||||
AssertionResult.setFailureMessage("Balance is not zero, but: "+ resp.balance);
|
||||
AssertionResult.setFailure(true)
|
||||
}
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223Assertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get payments" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/payments</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">${inkey}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check empty payment list" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="2914">[]</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
@@ -0,0 +1,487 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
|
||||
<hashTree>
|
||||
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
|
||||
<hashTree>
|
||||
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Server" enabled="true"/>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to home page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
<stringProp name="TestPlan.comments">Detected the start of a redirect chain</stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
<stringProp name="50554">307</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">40</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract `is_first_install` var" enabled="true">
|
||||
<stringProp name="RegexExtractor.useHeaders">true</stringProp>
|
||||
<stringProp name="RegexExtractor.refname">is_first_install</stringProp>
|
||||
<stringProp name="RegexExtractor.regex">location: (.*)</stringProp>
|
||||
<stringProp name="RegexExtractor.template">$1$</stringProp>
|
||||
<stringProp name="RegexExtractor.default">not-first-install</stringProp>
|
||||
<stringProp name="RegexExtractor.match_number">0</stringProp>
|
||||
<stringProp name="Sample.scope">all</stringProp>
|
||||
</RegexExtractor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="If First Install" enabled="true">
|
||||
<stringProp name="IfController.condition">${__groovy(vars.get('is_first_install').contains("first_install"),)}</stringProp>
|
||||
<boolProp name="IfController.evaluateAll">false</boolProp>
|
||||
<boolProp name="IfController.useExpression">true</boolProp>
|
||||
</IfController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to first_install" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/first_install</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Init super user" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"username":"admin","password":"admin_password","password_repeat":"admin_password"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/first_install</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/first_install</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="Else (not first install)" enabled="true">
|
||||
<stringProp name="IfController.condition">${__groovy(!vars.get('is_first_install').contains("first_install"),)}</stringProp>
|
||||
<boolProp name="IfController.evaluateAll">false</boolProp>
|
||||
<boolProp name="IfController.useExpression">true</boolProp>
|
||||
</IfController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Login super user" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"username":"admin","password":"admin_password"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Get all admin wallets" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/wallets</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract admin wallet and keys" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())[0]
|
||||
|
||||
vars.put("adminWalletId", resp.id || "no-admin-wallet-id");
|
||||
vars.put("adminWalletKey", resp.adminkey || 'no-adminkey');
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Top up wallet" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"amount":"1000000","id":"${adminWalletId}"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/admin/api/v1/topup/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?&wal=c1daa33fc4014ef69cd1505f14f322c2</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Logout super user" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/logout</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+83
-36
@@ -10,41 +10,53 @@ import traceback
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Callable, List
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
from slowapi import Limiter
|
||||
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,
|
||||
register_killswitch,
|
||||
register_task_listeners,
|
||||
from lnbits.core.tasks import ( # watchdog_task
|
||||
killswitch_task,
|
||||
wait_for_paid_invoices,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import cancel_all_tasks, create_permanent_task
|
||||
from lnbits.tasks import (
|
||||
cancel_all_tasks,
|
||||
create_permanent_task,
|
||||
register_invoice_listener,
|
||||
)
|
||||
from lnbits.utils.cache import cache
|
||||
from lnbits.wallets import get_wallet_class, set_wallet_class
|
||||
|
||||
from .commands import db_versions, load_disabled_extension_list, 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
|
||||
from .core.views.api import add_installed_extension
|
||||
from .core.views.generic import update_installed_extension_state
|
||||
from .extension_manager import Extension, InstallableExtension, get_valid_extensions
|
||||
from .extension_manager import (
|
||||
Extension,
|
||||
InstallableExtension,
|
||||
get_valid_extensions,
|
||||
version_parse,
|
||||
)
|
||||
from .helpers import template_renderer
|
||||
from .middleware import (
|
||||
CustomGZipMiddleware,
|
||||
ExtensionsRedirectMiddleware,
|
||||
InstalledExtensionMiddleware,
|
||||
add_first_install_middleware,
|
||||
add_ip_block_middleware,
|
||||
add_ratelimit_middleware,
|
||||
)
|
||||
@@ -53,7 +65,6 @@ from .tasks import (
|
||||
check_pending_payments,
|
||||
internal_invoice_listener,
|
||||
invoice_listener,
|
||||
webhook_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -76,12 +87,10 @@ def create_app() -> FastAPI:
|
||||
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
|
||||
setattr(core_app_extra, "register_new_ratelimiter", register_new_ratelimiter(app))
|
||||
|
||||
app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static")
|
||||
app.mount(
|
||||
"/core/static",
|
||||
StaticFiles(packages=[("lnbits.core", "static")]),
|
||||
name="core_static",
|
||||
)
|
||||
# register static files
|
||||
static_path = Path("lnbits", "static")
|
||||
static = StaticFiles(directory=static_path)
|
||||
app.mount("/static", static, name="static")
|
||||
|
||||
g().base_url = f"http://{settings.host}:{settings.port}"
|
||||
|
||||
@@ -93,18 +102,22 @@ def create_app() -> FastAPI:
|
||||
CustomGZipMiddleware, minimum_size=1000, exclude_paths=["/api/v1/payments/sse"]
|
||||
)
|
||||
|
||||
# required for SSO login
|
||||
app.add_middleware(SessionMiddleware, secret_key=settings.auth_secret_key)
|
||||
|
||||
# order of these two middlewares is important
|
||||
app.add_middleware(InstalledExtensionMiddleware)
|
||||
app.add_middleware(ExtensionsRedirectMiddleware)
|
||||
|
||||
register_custom_extensions_path()
|
||||
|
||||
add_first_install_middleware(app)
|
||||
|
||||
# adds security middleware
|
||||
add_ip_block_middleware(app)
|
||||
add_ratelimit_middleware(app)
|
||||
|
||||
register_startup(app)
|
||||
register_routes(app)
|
||||
register_async_tasks(app)
|
||||
register_exception_handlers(app)
|
||||
register_shutdown(app)
|
||||
@@ -159,7 +172,7 @@ async def check_funding_source() -> None:
|
||||
|
||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||
|
||||
logger.info(
|
||||
logger.success(
|
||||
f"✔️ Backend {WALLET.__class__.__name__} connected "
|
||||
f"and with a balance of {balance} msat."
|
||||
)
|
||||
@@ -181,12 +194,11 @@ async def check_installed_extensions(app: FastAPI):
|
||||
persist state. Zips that are missing will be re-downloaded.
|
||||
"""
|
||||
shutil.rmtree(os.path.join("lnbits", "upgrades"), True)
|
||||
await load_disabled_extension_list()
|
||||
installed_extensions = await build_all_installed_extensions_list()
|
||||
installed_extensions = await build_all_installed_extensions_list(False)
|
||||
|
||||
for ext in installed_extensions:
|
||||
try:
|
||||
installed = check_installed_extension_files(ext)
|
||||
installed = await check_installed_extension_files(ext)
|
||||
if not installed:
|
||||
await restore_installed_extension(app, ext)
|
||||
logger.info(
|
||||
@@ -204,7 +216,9 @@ async def check_installed_extensions(app: FastAPI):
|
||||
logger.info(f"{ext.id} ({ext.installed_version})")
|
||||
|
||||
|
||||
async def build_all_installed_extensions_list() -> List[InstallableExtension]:
|
||||
async def build_all_installed_extensions_list(
|
||||
include_deactivated: Optional[bool] = True,
|
||||
) -> List[InstallableExtension]:
|
||||
"""
|
||||
Returns a list of all the installed extensions plus the extensions that
|
||||
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
|
||||
@@ -217,7 +231,11 @@ async def build_all_installed_extensions_list() -> List[InstallableExtension]:
|
||||
continue
|
||||
|
||||
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
|
||||
release = ext_releases[0] if len(ext_releases) else None
|
||||
ext_releases = sorted(
|
||||
ext_releases, key=lambda r: version_parse(r.version), reverse=True
|
||||
)
|
||||
|
||||
release = next((e for e in ext_releases if e.is_version_compatible), None)
|
||||
|
||||
if release:
|
||||
ext_info = InstallableExtension(
|
||||
@@ -225,17 +243,27 @@ async def build_all_installed_extensions_list() -> List[InstallableExtension]:
|
||||
)
|
||||
installed_extensions.append(ext_info)
|
||||
|
||||
return installed_extensions
|
||||
if include_deactivated:
|
||||
return installed_extensions
|
||||
|
||||
if settings.lnbits_extensions_deactivate_all:
|
||||
return []
|
||||
|
||||
return [
|
||||
e
|
||||
for e in installed_extensions
|
||||
if e.id not in settings.lnbits_deactivated_extensions
|
||||
]
|
||||
|
||||
|
||||
def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
async def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
if ext.has_installed_version:
|
||||
return True
|
||||
|
||||
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
|
||||
|
||||
if f"./{str(ext.zip_path)}" not in zip_files:
|
||||
ext.download_archive()
|
||||
await ext.download_archive()
|
||||
ext.extract_archive()
|
||||
|
||||
return False
|
||||
@@ -248,7 +276,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
|
||||
@@ -261,7 +289,7 @@ def register_routes(app: FastAPI) -> None:
|
||||
"""Register FastAPI routes / LNbits extensions."""
|
||||
init_core_routers(app)
|
||||
|
||||
for ext in get_valid_extensions():
|
||||
for ext in get_valid_extensions(False):
|
||||
try:
|
||||
register_ext_routes(app, ext)
|
||||
except Exception as e:
|
||||
@@ -361,8 +389,7 @@ def register_startup(app: FastAPI):
|
||||
set_wallet_class()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error initializing {settings.lnbits_backend_wallet_class}: "
|
||||
f"{str(e)}"
|
||||
f"Error initializing {settings.lnbits_backend_wallet_class}: {e}"
|
||||
)
|
||||
set_void_wallet_class()
|
||||
|
||||
@@ -372,6 +399,9 @@ def register_startup(app: FastAPI):
|
||||
# check extensions after restart
|
||||
await check_installed_extensions(app)
|
||||
|
||||
# register core and extension routes
|
||||
register_routes(app)
|
||||
|
||||
if settings.lnbits_admin_ui:
|
||||
initialize_server_logger()
|
||||
|
||||
@@ -420,6 +450,8 @@ def log_server_info():
|
||||
logger.info(f"Data folder: {settings.lnbits_data_folder}")
|
||||
logger.info(f"Database: {get_db_vendor_name()}")
|
||||
logger.info(f"Service fee: {settings.lnbits_service_fee}")
|
||||
logger.info(f"Service fee max: {settings.lnbits_service_fee_max}")
|
||||
logger.info(f"Service fee wallet: {settings.lnbits_service_fee_wallet}")
|
||||
|
||||
|
||||
def get_db_vendor_name():
|
||||
@@ -436,19 +468,21 @@ def get_db_vendor_name():
|
||||
|
||||
|
||||
def register_async_tasks(app):
|
||||
@app.route("/wallet/webhook")
|
||||
async def webhook_listener():
|
||||
return await webhook_handler()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def listeners():
|
||||
create_permanent_task(check_pending_payments)
|
||||
create_permanent_task(invoice_listener)
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
create_permanent_task(cache.invalidate_forever)
|
||||
register_task_listeners()
|
||||
register_killswitch()
|
||||
# await run_deferred_async() # calle: doesn't do anyting?
|
||||
|
||||
# core invoice listener
|
||||
invoice_queue = asyncio.Queue(5)
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
# TODO: implement watchdog properly
|
||||
# create_permanent_task(watchdog_task)
|
||||
create_permanent_task(killswitch_task)
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI):
|
||||
@@ -507,6 +541,13 @@ def register_exception_handlers(app: FastAPI):
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
if exc.headers and "token-expired" in exc.headers:
|
||||
response = RedirectResponse("/")
|
||||
response.delete_cookie("cookie_access_token")
|
||||
response.delete_cookie("is_lnbits_user_authorized")
|
||||
response.set_cookie("is_access_token_expired", "true")
|
||||
return response
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html",
|
||||
{
|
||||
@@ -548,6 +589,12 @@ def configure_logger() -> None:
|
||||
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.error").propagate = False
|
||||
|
||||
logging.getLogger("sqlalchemy").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base").propagate = False
|
||||
logging.getLogger("sqlalchemy.engine.base.Engine").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base.Engine").propagate = False
|
||||
|
||||
|
||||
class Formatter:
|
||||
def __init__(self):
|
||||
|
||||
+489
-21
@@ -1,17 +1,47 @@
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
import httpx
|
||||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
from packaging import version
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.services import check_admin_settings
|
||||
from lnbits.core.views.api import api_install_extension, api_uninstall_extension
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .core import db as core_db
|
||||
from .core import migrations as core_migrations
|
||||
from .core.crud import get_dbversions, get_inactive_extensions
|
||||
from .core.crud import (
|
||||
delete_accounts_no_wallets,
|
||||
delete_unused_wallets,
|
||||
get_dbversions,
|
||||
get_inactive_extensions,
|
||||
get_installed_extension,
|
||||
get_installed_extensions,
|
||||
remove_deleted_wallets,
|
||||
)
|
||||
from .core.helpers import migrate_extension_database, run_migration
|
||||
from .db import COCKROACH, POSTGRES, SQLITE
|
||||
from .extension_manager import get_valid_extensions
|
||||
from .extension_manager import (
|
||||
CreateExtension,
|
||||
ExtensionRelease,
|
||||
InstallableExtension,
|
||||
get_valid_extensions,
|
||||
)
|
||||
|
||||
|
||||
def coro(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
return asyncio.run(f(*args, **kwargs))
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -28,34 +58,52 @@ def db():
|
||||
"""
|
||||
|
||||
|
||||
def get_super_user() -> str:
|
||||
@lnbits_cli.group()
|
||||
def extensions():
|
||||
"""
|
||||
Extensions related commands
|
||||
"""
|
||||
|
||||
|
||||
def get_super_user() -> Optional[str]:
|
||||
"""Get the superuser"""
|
||||
with open(Path(settings.lnbits_data_folder) / ".super_user", "r") as file:
|
||||
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
||||
if not superuser_file.exists() or not superuser_file.is_file():
|
||||
raise ValueError(
|
||||
"Superuser id not found. Please check that the file "
|
||||
+ f"'{superuser_file.absolute()}' exists and has read permissions."
|
||||
)
|
||||
with open(superuser_file, "r") as file:
|
||||
return file.readline()
|
||||
|
||||
|
||||
@lnbits_cli.command("superuser")
|
||||
def superuser():
|
||||
"""Prints the superuser"""
|
||||
click.echo(get_super_user())
|
||||
try:
|
||||
click.echo(get_super_user())
|
||||
except ValueError as e:
|
||||
click.echo(str(e))
|
||||
|
||||
|
||||
@lnbits_cli.command("superuser-url")
|
||||
def superuser_url():
|
||||
"""Prints the superuser"""
|
||||
click.echo(f"http://{settings.host}:{settings.port}/wallet?usr={get_super_user()}")
|
||||
try:
|
||||
click.echo(
|
||||
f"http://{settings.host}:{settings.port}/wallet?usr={get_super_user()}"
|
||||
)
|
||||
except ValueError as e:
|
||||
click.echo(str(e))
|
||||
|
||||
|
||||
@lnbits_cli.command("delete-settings")
|
||||
def delete_settings():
|
||||
@coro
|
||||
async def delete_settings():
|
||||
"""Deletes the settings"""
|
||||
|
||||
async def wrap():
|
||||
async with core_db.connect() as conn:
|
||||
await conn.execute("DELETE from settings")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(wrap())
|
||||
async with core_db.connect() as conn:
|
||||
await conn.execute("DELETE from settings")
|
||||
|
||||
|
||||
@db.command("migrate")
|
||||
@@ -91,7 +139,11 @@ async def migrate_databases():
|
||||
core_version = current_versions.get("core", 0)
|
||||
await run_migration(conn, core_migrations, "core", core_version)
|
||||
|
||||
for ext in get_valid_extensions():
|
||||
# here is the first place we can be sure that the
|
||||
# `installed_extensions` table has been created
|
||||
await load_disabled_extension_list()
|
||||
|
||||
for ext in get_valid_extensions(False):
|
||||
current_version = current_versions.get(ext.code, 0)
|
||||
try:
|
||||
await migrate_extension_database(ext, current_version)
|
||||
@@ -102,16 +154,41 @@ async def migrate_databases():
|
||||
|
||||
|
||||
@db.command("versions")
|
||||
def database_versions():
|
||||
"""Show current database versions"""
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(db_versions())
|
||||
|
||||
|
||||
@coro
|
||||
async def db_versions():
|
||||
"""Show current database versions"""
|
||||
async with core_db.connect() as conn:
|
||||
return await get_dbversions(conn)
|
||||
click.echo(await get_dbversions(conn))
|
||||
|
||||
|
||||
@db.command("cleanup-wallets")
|
||||
@click.argument("days", type=int, required=False)
|
||||
@coro
|
||||
async def database_cleanup_wallets(days: Optional[int] = None):
|
||||
"""Delete all wallets that never had any transaction"""
|
||||
async with core_db.connect() as conn:
|
||||
delta = days or settings.cleanup_wallets_days
|
||||
delta = delta * 24 * 60 * 60
|
||||
await delete_unused_wallets(delta, conn)
|
||||
|
||||
|
||||
@db.command("cleanup-deleted-wallets")
|
||||
@coro
|
||||
async def database_cleanup_deleted_wallets():
|
||||
"""Delete all wallets that has been marked deleted"""
|
||||
async with core_db.connect() as conn:
|
||||
await remove_deleted_wallets(conn)
|
||||
|
||||
|
||||
@db.command("cleanup-accounts")
|
||||
@click.argument("days", type=int, required=False)
|
||||
@coro
|
||||
async def database_cleanup_accounts(days: Optional[int] = None):
|
||||
"""Delete all accounts that have no wallets"""
|
||||
async with core_db.connect() as conn:
|
||||
delta = days or settings.cleanup_wallets_days
|
||||
delta = delta * 24 * 60 * 60
|
||||
await delete_accounts_no_wallets(delta, conn)
|
||||
|
||||
|
||||
async def load_disabled_extension_list() -> None:
|
||||
@@ -120,6 +197,184 @@ async def load_disabled_extension_list() -> None:
|
||||
settings.lnbits_deactivated_extensions += inactive_extensions
|
||||
|
||||
|
||||
@extensions.command("list")
|
||||
@coro
|
||||
async def extensions_list():
|
||||
"""Show currently installed extensions"""
|
||||
click.echo("Installed extensions:")
|
||||
|
||||
from lnbits.app import build_all_installed_extensions_list
|
||||
|
||||
for ext in await build_all_installed_extensions_list():
|
||||
assert ext.installed_release, f"Extension {ext.id} has no installed_release"
|
||||
click.echo(f" - {ext.id} ({ext.installed_release.version})")
|
||||
|
||||
|
||||
@extensions.command("update")
|
||||
@click.argument("extension", required=False)
|
||||
@click.option("-a", "--all", is_flag=True, help="Update all extensions.")
|
||||
@click.option(
|
||||
"-i", "--repo-index", help="Select the index of the repository to be used."
|
||||
)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--source-repo",
|
||||
help="""
|
||||
Provide the repository URL to be used for updating.
|
||||
The URL must be one present in `LNBITS_EXTENSIONS_MANIFESTS`
|
||||
or configured via the Admin UI. This option is required only
|
||||
if an extension is present in more than one repository.
|
||||
""",
|
||||
)
|
||||
@click.option(
|
||||
"-u",
|
||||
"--url",
|
||||
help="Use this option to update a running server. Eg: 'http://localhost:5000'.",
|
||||
)
|
||||
@click.option(
|
||||
"-d",
|
||||
"--admin-user",
|
||||
help="Admin user ID (must have permissions to install extensions).",
|
||||
)
|
||||
@coro
|
||||
async def extensions_update(
|
||||
extension: Optional[str] = None,
|
||||
all: Optional[bool] = False,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Update extension to the latest version.
|
||||
If an extension is not present it will be instaled.
|
||||
"""
|
||||
if not extension and not all:
|
||||
click.echo("Extension ID is required.")
|
||||
click.echo("Or specify the '--all' flag to update all extensions")
|
||||
return
|
||||
if extension and all:
|
||||
click.echo("Only one of extension ID or the '--all' flag must be specified")
|
||||
return
|
||||
if url and not _is_url(url):
|
||||
click.echo(f"Invalid '--url' option value: {url}")
|
||||
return
|
||||
|
||||
if not await _can_run_operation(url):
|
||||
return
|
||||
|
||||
if extension:
|
||||
await update_extension(extension, repo_index, source_repo, url, admin_user)
|
||||
return
|
||||
|
||||
click.echo("Updating all extensions...")
|
||||
installed_extensions = await get_installed_extensions()
|
||||
updated_extensions = []
|
||||
for e in installed_extensions:
|
||||
try:
|
||||
click.echo(f"""{"="*50} {e.id} {"="*(50-len(e.id))} """)
|
||||
success, msg = await update_extension(
|
||||
e.id, repo_index, source_repo, url, admin_user
|
||||
)
|
||||
if version:
|
||||
updated_extensions.append(
|
||||
{"id": e.id, "success": success, "message": msg}
|
||||
)
|
||||
except Exception as ex:
|
||||
click.echo(f"Failed to install extension '{e.id}': {ex}")
|
||||
|
||||
if len(updated_extensions) == 0:
|
||||
click.echo("No extension was updated.")
|
||||
return
|
||||
|
||||
for u in sorted(updated_extensions, key=lambda d: str(d["id"])):
|
||||
status = "updated to " if u["success"] else "not updated "
|
||||
click.echo(
|
||||
f"""'{u["id"]}' {" "*(20-len(str(u["id"])))}"""
|
||||
+ f""" - {status}: '{u["message"]}'"""
|
||||
)
|
||||
|
||||
|
||||
@extensions.command("install")
|
||||
@click.argument("extension")
|
||||
@click.option(
|
||||
"-i", "--repo-index", help="Select the index of the repository to be used."
|
||||
)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--source-repo",
|
||||
help="""
|
||||
Provide the repository URL to be used for updating.
|
||||
The URL must be one present in `LNBITS_EXTENSIONS_MANIFESTS`
|
||||
or configured via the Admin UI. This option is required only
|
||||
if an extension is present in more than one repository.
|
||||
""",
|
||||
)
|
||||
@click.option(
|
||||
"-u",
|
||||
"--url",
|
||||
help="Use this option to update a running server. Eg: 'http://localhost:5000'.",
|
||||
)
|
||||
@click.option(
|
||||
"-d",
|
||||
"--admin-user",
|
||||
help="Admin user ID (must have permissions to install extensions).",
|
||||
)
|
||||
@coro
|
||||
async def extensions_install(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
):
|
||||
"""Install a extension"""
|
||||
click.echo(f"Installing {extension}... {repo_index}")
|
||||
if url and not _is_url(url):
|
||||
click.echo(f"Invalid '--url' option value: {url}")
|
||||
return
|
||||
|
||||
if not await _can_run_operation(url):
|
||||
return
|
||||
await install_extension(extension, repo_index, source_repo, url, admin_user)
|
||||
|
||||
|
||||
@extensions.command("uninstall")
|
||||
@click.argument("extension")
|
||||
@click.option(
|
||||
"-u",
|
||||
"--url",
|
||||
help="Use this option to update a running server. Eg: 'http://localhost:5000'.",
|
||||
)
|
||||
@click.option(
|
||||
"-d",
|
||||
"--admin-user",
|
||||
help="Admin user ID (must have permissions to install extensions).",
|
||||
)
|
||||
@coro
|
||||
async def extensions_uninstall(
|
||||
extension: str, url: Optional[str] = None, admin_user: Optional[str] = None
|
||||
):
|
||||
"""Uninstall a extension"""
|
||||
click.echo(f"Uninstalling '{extension}'...")
|
||||
|
||||
if url and not _is_url(url):
|
||||
click.echo(f"Invalid '--url' option value: {url}")
|
||||
return
|
||||
|
||||
if not await _can_run_operation(url):
|
||||
return
|
||||
try:
|
||||
await _call_uninstall_extension(extension, url, admin_user)
|
||||
click.echo(f"Extension '{extension}' uninstalled.")
|
||||
except HTTPException as ex:
|
||||
click.echo(f"Failed to uninstall '{extension}' Error: '{ex.detail}'.")
|
||||
return False, ex.detail
|
||||
except Exception as ex:
|
||||
click.echo(f"Failed to uninstall '{extension}': {str(ex)}.")
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
def main():
|
||||
"""main function"""
|
||||
lnbits_cli()
|
||||
@@ -127,3 +382,216 @@ def main():
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
async def install_extension(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
try:
|
||||
release = await _select_release(extension, repo_index, source_repo)
|
||||
if not release:
|
||||
return False, "No release selected"
|
||||
|
||||
data = CreateExtension(
|
||||
ext_id=extension,
|
||||
archive=release.archive,
|
||||
source_repo=release.source_repo,
|
||||
version=release.version,
|
||||
)
|
||||
await _call_install_extension(data, url, admin_user)
|
||||
click.echo(f"Extension '{extension}' ({release.version}) installed.")
|
||||
return True, release.version
|
||||
except HTTPException as ex:
|
||||
click.echo(f"Failed to install '{extension}' Error: '{ex.detail}'.")
|
||||
return False, ex.detail
|
||||
except Exception as ex:
|
||||
click.echo(f"Failed to install '{extension}': {str(ex)}.")
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
async def update_extension(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
try:
|
||||
click.echo(f"Updating '{extension}' extension.")
|
||||
installed_ext = await get_installed_extension(extension)
|
||||
if not installed_ext:
|
||||
click.echo(
|
||||
f"Extension '{extension}' is not installed. Preparing to install..."
|
||||
)
|
||||
return await install_extension(extension, repo_index, source_repo, url)
|
||||
|
||||
click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.")
|
||||
|
||||
assert (
|
||||
installed_ext.installed_release
|
||||
), "Cannot find previously installed release. Please uninstall first."
|
||||
|
||||
release = await _select_release(extension, repo_index, source_repo)
|
||||
if not release:
|
||||
return False, "No release selected."
|
||||
if (
|
||||
release.version == installed_ext.installed_version
|
||||
and release.source_repo == installed_ext.installed_release.source_repo
|
||||
):
|
||||
click.echo(f"Extension '{extension}' already up to date.")
|
||||
return False, "Already up to date"
|
||||
|
||||
click.echo(f"Updating '{extension}' extension to version: {release.version }")
|
||||
|
||||
data = CreateExtension(
|
||||
ext_id=extension,
|
||||
archive=release.archive,
|
||||
source_repo=release.source_repo,
|
||||
version=release.version,
|
||||
)
|
||||
|
||||
await _call_install_extension(data, url, admin_user)
|
||||
click.echo(f"Extension '{extension}' updated.")
|
||||
return True, release.version
|
||||
except HTTPException as ex:
|
||||
click.echo(f"Failed to update '{extension}' Error: '{ex.detail}.")
|
||||
return False, ex.detail
|
||||
except Exception as ex:
|
||||
click.echo(f"Failed to update '{extension}': {str(ex)}.")
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
async def _select_release(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
) -> Optional[ExtensionRelease]:
|
||||
all_releases = await InstallableExtension.get_extension_releases(extension)
|
||||
if len(all_releases) == 0:
|
||||
click.echo(f"No repository found for extension '{extension}'.")
|
||||
return None
|
||||
|
||||
latest_repo_releases = _get_latest_release_per_repo(all_releases)
|
||||
|
||||
if source_repo:
|
||||
if source_repo not in latest_repo_releases:
|
||||
click.echo(f"Repository not found: '{source_repo}'")
|
||||
return None
|
||||
return latest_repo_releases[source_repo]
|
||||
|
||||
if len(latest_repo_releases) == 1:
|
||||
return latest_repo_releases[list(latest_repo_releases.keys())[0]]
|
||||
|
||||
repos = list(latest_repo_releases.keys())
|
||||
repos.sort()
|
||||
if not repo_index:
|
||||
click.echo("Multiple repos found. Please select one using:")
|
||||
click.echo(" --repo-index option for index of the repo, or")
|
||||
click.echo(" --source-repo option for the manifest URL")
|
||||
click.echo("")
|
||||
click.echo("Repositories: ")
|
||||
|
||||
for index, repo in enumerate(repos):
|
||||
release = latest_repo_releases[repo]
|
||||
click.echo(f" [{index}] {repo} --> {release.version}")
|
||||
click.echo("")
|
||||
return None
|
||||
|
||||
if not repo_index.isnumeric() or not 0 <= int(repo_index) < len(repos):
|
||||
click.echo(f"--repo-index must be between '0' and '{len(repos) - 1}'")
|
||||
return None
|
||||
|
||||
return latest_repo_releases[repos[int(repo_index)]]
|
||||
|
||||
|
||||
def _get_latest_release_per_repo(all_releases):
|
||||
latest_repo_releases = {}
|
||||
for release in all_releases:
|
||||
try:
|
||||
if not release.is_version_compatible:
|
||||
continue
|
||||
# do not remove, parsing also validates
|
||||
release_version = version.parse(release.version)
|
||||
if release.source_repo not in latest_repo_releases:
|
||||
latest_repo_releases[release.source_repo] = release
|
||||
continue
|
||||
if release_version > version.parse(
|
||||
latest_repo_releases[release.source_repo].version
|
||||
):
|
||||
latest_repo_releases[release.source_repo] = release
|
||||
except version.InvalidVersion as ex:
|
||||
logger.warning(f"Invalid version {release.name}: {ex}")
|
||||
return latest_repo_releases
|
||||
|
||||
|
||||
async def _call_install_extension(
|
||||
data: CreateExtension, url: Optional[str], user_id: Optional[str] = None
|
||||
):
|
||||
if url:
|
||||
user_id = user_id or get_super_user()
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{url}/api/v1/extension?usr={user_id}", json=data.dict(), timeout=40
|
||||
)
|
||||
resp.raise_for_status()
|
||||
else:
|
||||
await api_install_extension(data, User(id="mock_id"))
|
||||
|
||||
|
||||
async def _call_uninstall_extension(
|
||||
extension: str, url: Optional[str], user_id: Optional[str] = None
|
||||
):
|
||||
if url:
|
||||
user_id = user_id or get_super_user()
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.delete(
|
||||
f"{url}/api/v1/extension/{extension}?usr={user_id}", timeout=40
|
||||
)
|
||||
resp.raise_for_status()
|
||||
else:
|
||||
await api_uninstall_extension(extension, User(id="mock_id"))
|
||||
|
||||
|
||||
async def _can_run_operation(url) -> bool:
|
||||
await check_admin_settings()
|
||||
if await _is_lnbits_started(url):
|
||||
if not url:
|
||||
click.echo("LNbits server is started. Please either:")
|
||||
click.echo(
|
||||
f" - use the '--url' option. Eg: --url=http://{settings.host}:{settings.port}"
|
||||
)
|
||||
click.echo(
|
||||
f" - stop the server running at 'http://{settings.host}:{settings.port}'"
|
||||
)
|
||||
|
||||
return False
|
||||
elif url:
|
||||
click.echo(
|
||||
"The option '--url' has been provided,"
|
||||
+ f" but no server found runnint at '{url}'"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _is_lnbits_started(url: Optional[str]):
|
||||
try:
|
||||
url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.get(url)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _is_url(url):
|
||||
try:
|
||||
result = urlparse(url)
|
||||
return all([result.scheme, result.netloc])
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -3,11 +3,14 @@ from fastapi import APIRouter
|
||||
from .db import core_app_extra, db
|
||||
from .views.admin_api import admin_router
|
||||
from .views.api import api_router
|
||||
from .views.auth_api import auth_router
|
||||
|
||||
# this compat is needed for usermanager extension
|
||||
from .views.generic import generic_router, update_user_extension
|
||||
from .views.node_api import node_router, public_node_router, super_node_router
|
||||
from .views.public_api import public_router
|
||||
from .views.tinyurl_api import tinyurl_router
|
||||
from .views.webpush_api import webpush_router
|
||||
|
||||
# backwards compatibility for extensions
|
||||
core_app = APIRouter(tags=["Core"])
|
||||
@@ -22,3 +25,6 @@ def init_core_routers(app):
|
||||
app.include_router(super_node_router)
|
||||
app.include_router(public_node_router)
|
||||
app.include_router(admin_router)
|
||||
app.include_router(tinyurl_router)
|
||||
app.include_router(webpush_router)
|
||||
app.include_router(auth_router)
|
||||
|
||||
+270
-14
@@ -1,10 +1,12 @@
|
||||
import datetime
|
||||
import json
|
||||
from time import time
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import shortuuid
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import WalletType
|
||||
@@ -20,11 +22,14 @@ from lnbits.settings import (
|
||||
|
||||
from .models import (
|
||||
BalanceCheck,
|
||||
CreateUser,
|
||||
Payment,
|
||||
PaymentFilters,
|
||||
PaymentHistoryPoint,
|
||||
TinyURL,
|
||||
UpdateUserPassword,
|
||||
User,
|
||||
UserConfig,
|
||||
Wallet,
|
||||
WebPushSubscription,
|
||||
)
|
||||
@@ -33,8 +38,48 @@ from .models import (
|
||||
# --------
|
||||
|
||||
|
||||
async def create_user(
|
||||
data: CreateUser, user_config: Optional[UserConfig] = None
|
||||
) -> User:
|
||||
if not settings.new_accounts_allowed:
|
||||
raise ValueError("Account creation is disabled.")
|
||||
if await get_account_by_username(data.username):
|
||||
raise ValueError("Username already exists.")
|
||||
|
||||
if data.email and await get_account_by_email(data.email):
|
||||
raise ValueError("Email already exists.")
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
user_id = uuid4().hex
|
||||
tsph = db.timestamp_placeholder
|
||||
now = int(time())
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO accounts
|
||||
(id, email, username, pass, extra, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, {tsph}, {tsph})
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
data.email,
|
||||
data.username,
|
||||
pwd_context.hash(data.password),
|
||||
json.dumps(dict(user_config)) if user_config else "{}",
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
new_account = await get_account(user_id=user_id)
|
||||
assert new_account, "Newly created account couldn't be retrieved"
|
||||
return new_account
|
||||
|
||||
|
||||
async def create_account(
|
||||
conn: Optional[Connection] = None, user_id: Optional[str] = None
|
||||
conn: Optional[Connection] = None,
|
||||
user_id: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
user_config: Optional[UserConfig] = None,
|
||||
) -> User:
|
||||
if user_id:
|
||||
user_uuid4 = UUID(hex=user_id, version=4)
|
||||
@@ -42,7 +87,15 @@ async def create_account(
|
||||
else:
|
||||
user_id = uuid4().hex
|
||||
|
||||
await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
|
||||
extra = json.dumps(dict(user_config)) if user_config else "{}"
|
||||
now = int(time())
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO accounts (id, email, extra, created_at, updated_at)
|
||||
VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
|
||||
""",
|
||||
(user_id, email, extra, now, now),
|
||||
)
|
||||
|
||||
new_account = await get_account(user_id=user_id, conn=conn)
|
||||
assert new_account, "Newly created account couldn't be retrieved"
|
||||
@@ -50,19 +103,176 @@ async def create_account(
|
||||
return new_account
|
||||
|
||||
|
||||
async def update_account(
|
||||
user_id: str,
|
||||
username: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
user_config: Optional[UserConfig] = None,
|
||||
) -> Optional[User]:
|
||||
user = await get_account(user_id)
|
||||
assert user, "User not found"
|
||||
|
||||
if email:
|
||||
assert not user.email or email == user.email, "Cannot change email."
|
||||
account = await get_account_by_email(email)
|
||||
assert not account or account.id == user_id, "Email already in use."
|
||||
|
||||
if username:
|
||||
assert not user.username or username == user.username, "Cannot change username."
|
||||
account = await get_account_by_username(username)
|
||||
assert not account or account.id == user_id, "Username already in exists."
|
||||
|
||||
username = user.username or username
|
||||
email = user.email or email
|
||||
extra = user_config or user.config
|
||||
|
||||
now = int(time())
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE accounts SET (username, email, extra, updated_at) =
|
||||
(?, ?, ?, {db.timestamp_placeholder})
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
username,
|
||||
email,
|
||||
json.dumps(dict(extra)) if extra else "{}",
|
||||
now,
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
|
||||
user = await get_user(user_id)
|
||||
assert user, "Updated account couldn't be retrieved"
|
||||
return user
|
||||
|
||||
|
||||
async def get_account(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
) -> Optional[User]:
|
||||
row = await (conn or db).fetchone(
|
||||
"SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)
|
||||
"""
|
||||
SELECT id, email, username, created_at, updated_at, extra
|
||||
FROM accounts WHERE id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
user = User(**row) if row else None
|
||||
if user and row["extra"]:
|
||||
user.config = UserConfig(**json.loads(row["extra"]))
|
||||
return user
|
||||
|
||||
|
||||
async def delete_accounts_no_wallets(
|
||||
time_delta: int,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
DELETE FROM accounts
|
||||
WHERE NOT EXISTS (
|
||||
SELECT wallets.id FROM wallets WHERE wallets.user = accounts.id
|
||||
) AND updated_at < {db.timestamp_placeholder}
|
||||
""",
|
||||
(int(time()) - time_delta,),
|
||||
)
|
||||
|
||||
|
||||
async def get_user_password(user_id: str) -> Optional[str]:
|
||||
row = await db.fetchone(
|
||||
"SELECT pass FROM accounts WHERE id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return row[0]
|
||||
|
||||
|
||||
async def verify_user_password(user_id: str, password: str) -> bool:
|
||||
existing_password = await get_user_password(user_id)
|
||||
if not existing_password:
|
||||
return False
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
return pwd_context.verify(password, existing_password)
|
||||
|
||||
|
||||
# todo: , conn: Optional[Connection] = None ??
|
||||
async def update_user_password(data: UpdateUserPassword) -> Optional[User]:
|
||||
assert data.password == data.password_repeat, "Passwords do not match."
|
||||
|
||||
# old accounts do not have a pasword
|
||||
if await get_user_password(data.user_id):
|
||||
assert data.password_old, "Missing old password"
|
||||
old_pwd_ok = await verify_user_password(data.user_id, data.password_old)
|
||||
assert old_pwd_ok, "Invalid credentials."
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
now = int(time())
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE accounts SET pass = ?, updated_at = {db.timestamp_placeholder}
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
pwd_context.hash(data.password),
|
||||
now,
|
||||
data.user_id,
|
||||
),
|
||||
)
|
||||
|
||||
user = await get_user(data.user_id)
|
||||
assert user, "Updated account couldn't be retrieved"
|
||||
return user
|
||||
|
||||
|
||||
async def get_account_by_username(
|
||||
username: str, conn: Optional[Connection] = None
|
||||
) -> Optional[User]:
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT id, username, email, created_at, updated_at
|
||||
FROM accounts WHERE username = ?
|
||||
""",
|
||||
(username,),
|
||||
)
|
||||
|
||||
return User(**row) if row else None
|
||||
|
||||
|
||||
async def get_account_by_email(
|
||||
email: str, conn: Optional[Connection] = None
|
||||
) -> Optional[User]:
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT id, username, email, created_at, updated_at
|
||||
FROM accounts WHERE email = ?
|
||||
""",
|
||||
(email,),
|
||||
)
|
||||
|
||||
return User(**row) if row else None
|
||||
|
||||
|
||||
async def get_account_by_username_or_email(
|
||||
username_or_email: str, conn: Optional[Connection] = None
|
||||
) -> Optional[User]:
|
||||
user = await get_account_by_username(username_or_email, conn)
|
||||
if not user:
|
||||
user = await get_account_by_email(username_or_email, conn)
|
||||
return user
|
||||
|
||||
|
||||
async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]:
|
||||
user = await (conn or db).fetchone(
|
||||
"SELECT id, email FROM accounts WHERE id = ?", (user_id,)
|
||||
"""
|
||||
SELECT id, email, username, pass, extra, created_at, updated_at
|
||||
FROM accounts WHERE id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
if user:
|
||||
@@ -86,6 +296,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||
return User(
|
||||
id=user["id"],
|
||||
email=user["email"],
|
||||
username=user["username"],
|
||||
extensions=[
|
||||
e[0] for e in extensions if User.is_extension_for_user(e[0], user["id"])
|
||||
],
|
||||
@@ -93,6 +304,8 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||
admin=user["id"] == settings.super_user
|
||||
or user["id"] in settings.lnbits_admin_users,
|
||||
super_user=user["id"] == settings.super_user,
|
||||
has_password=True if user["pass"] else False,
|
||||
config=UserConfig(**json.loads(user["extra"])) if user["extra"] else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -109,6 +322,7 @@ async def add_installed_extension(
|
||||
dict(ext.installed_release) if ext.installed_release else None
|
||||
),
|
||||
"dependencies": ext.dependencies,
|
||||
"payments": [dict(p) for p in ext.payments] if ext.payments else None,
|
||||
}
|
||||
|
||||
version = ext.installed_release.version if ext.installed_release else ""
|
||||
@@ -181,13 +395,15 @@ async def drop_extension_db(*, ext_id: str, conn: Optional[Connection] = None) -
|
||||
)
|
||||
|
||||
|
||||
async def get_installed_extension(ext_id: str, conn: Optional[Connection] = None):
|
||||
async def get_installed_extension(
|
||||
ext_id: str, conn: Optional[Connection] = None
|
||||
) -> Optional[InstallableExtension]:
|
||||
row = await (conn or db).fetchone(
|
||||
"SELECT * FROM installed_extensions WHERE id = ?",
|
||||
(ext_id,),
|
||||
)
|
||||
|
||||
return dict(row) if row else None
|
||||
return InstallableExtension.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_installed_extensions(
|
||||
@@ -231,10 +447,11 @@ async def create_wallet(
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Wallet:
|
||||
wallet_id = uuid4().hex
|
||||
now = int(time())
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO wallets (id, name, "user", adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
f"""
|
||||
INSERT INTO wallets (id, name, "user", adminkey, inkey, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
|
||||
""",
|
||||
(
|
||||
wallet_id,
|
||||
@@ -242,6 +459,8 @@ async def create_wallet(
|
||||
user_id,
|
||||
uuid4().hex,
|
||||
uuid4().hex,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -258,7 +477,10 @@ async def update_wallet(
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[Wallet]:
|
||||
set_clause = []
|
||||
values = []
|
||||
values: list = []
|
||||
set_clause.append(f"updated_at = {db.timestamp_placeholder}")
|
||||
now = int(time())
|
||||
values.append(now)
|
||||
if name:
|
||||
set_clause.append("name = ?")
|
||||
values.append(name)
|
||||
@@ -280,13 +502,33 @@ async def update_wallet(
|
||||
async def delete_wallet(
|
||||
*, user_id: str, wallet_id: str, conn: Optional[Connection] = None
|
||||
) -> None:
|
||||
now = int(time())
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
f"""
|
||||
UPDATE wallets
|
||||
SET deleted = true
|
||||
SET deleted = true, updated_at = {db.timestamp_placeholder}
|
||||
WHERE id = ? AND "user" = ?
|
||||
""",
|
||||
(wallet_id, user_id),
|
||||
(now, wallet_id, user_id),
|
||||
)
|
||||
|
||||
|
||||
async def remove_deleted_wallets(conn: Optional[Connection] = None) -> None:
|
||||
await (conn or db).execute("DELETE FROM wallets WHERE deleted = true")
|
||||
|
||||
|
||||
async def delete_unused_wallets(
|
||||
time_delta: int,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
DELETE FROM wallets
|
||||
WHERE (
|
||||
SELECT COUNT(*) FROM apipayments WHERE wallet = wallets.id
|
||||
) = 0 AND updated_at < {db.timestamp_placeholder}
|
||||
""",
|
||||
(int(time()) - time_delta,),
|
||||
)
|
||||
|
||||
|
||||
@@ -312,7 +554,8 @@ async def get_wallet_for_key(
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
|
||||
AS balance_msat FROM wallets WHERE adminkey = ? OR inkey = ?
|
||||
AS balance_msat FROM wallets
|
||||
WHERE (adminkey = ? OR inkey = ?) AND deleted = false
|
||||
""",
|
||||
(key, key),
|
||||
)
|
||||
@@ -360,6 +603,7 @@ async def get_standalone_payment(
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE {clause}
|
||||
ORDER BY amount
|
||||
LIMIT 1
|
||||
""",
|
||||
tuple(values),
|
||||
@@ -771,6 +1015,16 @@ async def check_internal_pending(
|
||||
return row["pending"]
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment_hash: str, status: int) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET webhook_status = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(status, payment_hash),
|
||||
)
|
||||
|
||||
|
||||
# balance_check
|
||||
# -------------
|
||||
|
||||
@@ -856,6 +1110,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,
|
||||
|
||||
+51
-11
@@ -1,6 +1,6 @@
|
||||
import importlib
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
import httpx
|
||||
@@ -48,24 +48,64 @@ async def run_migration(
|
||||
await update_migration_version(conn, db_name, version)
|
||||
|
||||
|
||||
async def stop_extension_background_work(ext_id: str, user: str):
|
||||
async def stop_extension_background_work(
|
||||
ext_id: str, user: str, access_token: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extensions SHOULD expose a DELETE enpoint at the root level of their API.
|
||||
This function tries first to call the endpoint using `http`
|
||||
and if it fails it tries using `https`.
|
||||
Extensions SHOULD expose a `api_stop()` function and/or a DELETE enpoint
|
||||
at the root level of their API.
|
||||
"""
|
||||
stopped = await _stop_extension_background_work(ext_id)
|
||||
|
||||
if not stopped:
|
||||
# fallback to REST API call
|
||||
await _stop_extension_background_work_via_api(ext_id, user, access_token)
|
||||
|
||||
|
||||
async def _stop_extension_background_work(ext_id) -> bool:
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id) or ""
|
||||
ext = Extension(ext_id, True, False, upgrade_hash=upgrade_hash)
|
||||
|
||||
try:
|
||||
logger.info(f"Stopping background work for extension '{ext.module_name}'.")
|
||||
old_module = importlib.import_module(ext.module_name)
|
||||
|
||||
# Extensions must expose an `{ext_id}_stop()` function at the module level
|
||||
# The `api_stop()` function is for backwards compatibility (will be deprecated)
|
||||
stop_fns = [f"{ext_id}_stop", "api_stop"]
|
||||
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
|
||||
assert stop_fn_name, "No stop function found for '{ext.module_name}'"
|
||||
|
||||
await getattr(old_module, stop_fn_name)()
|
||||
|
||||
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
|
||||
except Exception as ex:
|
||||
logger.warning(f"Failed to stop background work for '{ext.module_name}'.")
|
||||
logger.warning(ex)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _stop_extension_background_work_via_api(ext_id, user, access_token):
|
||||
logger.info(
|
||||
f"Stopping background work for extension '{ext_id}' using the REST API."
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
url = f"http://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
|
||||
await client.delete(url)
|
||||
headers = (
|
||||
{"Authorization": "Bearer " + access_token} if access_token else None
|
||||
)
|
||||
resp = await client.delete(url=url, headers=headers)
|
||||
resp.raise_for_status()
|
||||
logger.info(f"Stopped background work for extension '{ext_id}'.")
|
||||
except Exception as ex:
|
||||
logger.warning(
|
||||
f"Failed to stop background work for '{ext_id}' using the REST API."
|
||||
)
|
||||
logger.warning(ex)
|
||||
try:
|
||||
# try https
|
||||
url = f"https://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
def to_valid_user_id(user_id: str) -> UUID:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
from time import time
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy.exc import OperationalError
|
||||
@@ -393,3 +394,80 @@ async def m015_create_push_notification_subscriptions_table(db):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m016_add_username_column_to_accounts(db):
|
||||
"""
|
||||
Adds username column to accounts.
|
||||
"""
|
||||
try:
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN username TEXT")
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN extra TEXT")
|
||||
except OperationalError:
|
||||
pass
|
||||
|
||||
|
||||
async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
|
||||
"""
|
||||
Adds created_at and updated_at column to accounts and wallets.
|
||||
"""
|
||||
try:
|
||||
await db.execute(
|
||||
"ALTER TABLE accounts "
|
||||
f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE accounts "
|
||||
f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE wallets "
|
||||
f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE wallets "
|
||||
f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
|
||||
)
|
||||
|
||||
# # set their wallets created_at with the first payment
|
||||
# await db.execute(
|
||||
# """
|
||||
# UPDATE wallets SET created_at = (
|
||||
# SELECT time FROM apipayments
|
||||
# WHERE apipayments.wallet = wallets.id
|
||||
# ORDER BY time ASC LIMIT 1
|
||||
# )
|
||||
# """
|
||||
# )
|
||||
|
||||
# # then set their accounts created_at with the wallet
|
||||
# await db.execute(
|
||||
# """
|
||||
# UPDATE accounts SET created_at = (
|
||||
# SELECT created_at FROM wallets
|
||||
# WHERE wallets.user = accounts.id
|
||||
# ORDER BY created_at ASC LIMIT 1
|
||||
# )
|
||||
# """
|
||||
# )
|
||||
|
||||
# set all to now where they are null
|
||||
now = int(time())
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE wallets SET created_at = {db.timestamp_placeholder}
|
||||
WHERE created_at IS NULL
|
||||
""",
|
||||
(now,),
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE accounts SET created_at = {db.timestamp_placeholder}
|
||||
WHERE created_at IS NULL
|
||||
""",
|
||||
(now,),
|
||||
)
|
||||
|
||||
except OperationalError as exc:
|
||||
logger.error(f"Migration 17 failed: {exc}")
|
||||
pass
|
||||
|
||||
+83
-12
@@ -10,26 +10,31 @@ from typing import Callable, Dict, List, Optional
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey
|
||||
from fastapi import Query
|
||||
from lnurl import encode as lnurl_encode
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.db import Connection, FilterModel, FromRowModel
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.lnurl import encode as lnurl_encode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
from lnbits.wallets.base import PaymentStatus
|
||||
from lnbits.wallets.base import PaymentPendingStatus, PaymentStatus
|
||||
|
||||
|
||||
class Wallet(BaseModel):
|
||||
class BaseWallet(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
user: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
currency: Optional[str]
|
||||
balance_msat: int
|
||||
|
||||
|
||||
class Wallet(BaseWallet):
|
||||
user: str
|
||||
currency: Optional[str]
|
||||
deleted: bool
|
||||
created_at: Optional[int] = None
|
||||
updated_at: Optional[int] = None
|
||||
|
||||
@property
|
||||
def balance(self) -> int:
|
||||
@@ -79,14 +84,31 @@ class WalletTypeInfo:
|
||||
wallet: Wallet
|
||||
|
||||
|
||||
class UserConfig(BaseModel):
|
||||
email_verified: Optional[bool] = False
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
picture: Optional[str] = None
|
||||
# Auth provider, possible values:
|
||||
# - "env": the user was created automatically by the system
|
||||
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
||||
# - "google | github | ...": the user was created using an SSO provider
|
||||
provider: Optional[str] = "lnbits" # auth provider
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
email: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
extensions: List[str] = []
|
||||
wallets: List[Wallet] = []
|
||||
password: Optional[str] = None
|
||||
admin: bool = False
|
||||
super_user: bool = False
|
||||
has_password: bool = False
|
||||
config: Optional[UserConfig] = None
|
||||
created_at: Optional[int] = None
|
||||
updated_at: Optional[int] = None
|
||||
|
||||
@property
|
||||
def wallet_ids(self) -> List[str]:
|
||||
@@ -107,6 +129,43 @@ class User(BaseModel):
|
||||
return False
|
||||
|
||||
|
||||
class CreateUser(BaseModel):
|
||||
email: Optional[str] = Query(default=None)
|
||||
username: str = Query(default=..., min_length=2, max_length=20)
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
|
||||
|
||||
class UpdateUser(BaseModel):
|
||||
user_id: str
|
||||
email: Optional[str] = Query(default=None)
|
||||
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
||||
config: Optional[UserConfig] = None
|
||||
|
||||
|
||||
class UpdateUserPassword(BaseModel):
|
||||
user_id: str
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_old: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
||||
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
||||
|
||||
|
||||
class UpdateSuperuserPassword(BaseModel):
|
||||
username: str = Query(default=..., min_length=2, max_length=20)
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
|
||||
|
||||
class LoginUsr(BaseModel):
|
||||
usr: str
|
||||
|
||||
|
||||
class LoginUsernamePassword(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class Payment(FromRowModel):
|
||||
checking_id: str
|
||||
pending: bool
|
||||
@@ -199,7 +258,7 @@ class Payment(FromRowModel):
|
||||
conn: Optional[Connection] = None,
|
||||
) -> PaymentStatus:
|
||||
if self.is_uncheckable:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
logger.debug(
|
||||
f"Checking {'outgoing' if self.is_out else 'incoming'} "
|
||||
@@ -221,11 +280,18 @@ class Payment(FromRowModel):
|
||||
f"expired {expiration_date}"
|
||||
)
|
||||
await self.delete(conn)
|
||||
# wait at least 15 minutes before deleting failed outgoing payments
|
||||
elif self.is_out and status.failed:
|
||||
logger.warning(
|
||||
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||
)
|
||||
await self.delete(conn)
|
||||
if self.time + 900 < int(time.time()):
|
||||
logger.warning(
|
||||
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||
)
|
||||
await self.delete(conn)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Tried to delete outgoing payment {self.checking_id}: "
|
||||
"skipping because it's not old enough"
|
||||
)
|
||||
elif not status.pending:
|
||||
logger.info(
|
||||
f"Marking '{'in' if self.is_in else 'out'}' "
|
||||
@@ -275,8 +341,12 @@ class BalanceCheck(BaseModel):
|
||||
return cls(wallet=row["wallet"], service=row["service"], url=row["url"])
|
||||
|
||||
|
||||
def _do_nothing(*_):
|
||||
pass
|
||||
|
||||
|
||||
class CoreAppExtra:
|
||||
register_new_ext_routes: Callable
|
||||
register_new_ext_routes: Callable = _do_nothing
|
||||
register_new_ratelimiter: Callable
|
||||
|
||||
|
||||
@@ -312,6 +382,7 @@ class CreateLnurl(BaseModel):
|
||||
amount: int
|
||||
comment: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
unit: Optional[str] = None
|
||||
|
||||
|
||||
class CreateInvoice(BaseModel):
|
||||
|
||||
+160
-33
@@ -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
|
||||
@@ -11,8 +12,6 @@ from bolt11 import Bolt11
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from fastapi import Depends, WebSocket
|
||||
from lnurl import LnurlErrorResponse
|
||||
from lnurl import decode as decode_lnurl
|
||||
from loguru import logger
|
||||
from py_vapid import Vapid
|
||||
from py_vapid.utils import b64urlencode
|
||||
@@ -21,6 +20,8 @@ from lnbits.core.db import db
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.lnurl import LnurlErrorResponse
|
||||
from lnbits.lnurl import decode as decode_lnurl
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
SuperSettings,
|
||||
@@ -30,7 +31,12 @@ from lnbits.settings import (
|
||||
)
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||
from lnbits.wallets import FAKE_WALLET, get_wallet_class, set_wallet_class
|
||||
from lnbits.wallets.base import PaymentResponse, PaymentStatus
|
||||
from lnbits.wallets.base import (
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
)
|
||||
|
||||
from .crud import (
|
||||
check_internal,
|
||||
@@ -41,6 +47,7 @@ from .crud import (
|
||||
create_wallet,
|
||||
delete_wallet_payment,
|
||||
get_account,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_super_settings,
|
||||
get_total_balance,
|
||||
@@ -52,7 +59,7 @@ from .crud import (
|
||||
update_super_user,
|
||||
)
|
||||
from .helpers import to_valid_user_id
|
||||
from .models import Payment, Wallet
|
||||
from .models import Payment, UserConfig, Wallet
|
||||
|
||||
|
||||
class PaymentFailure(Exception):
|
||||
@@ -118,8 +125,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 +138,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,
|
||||
@@ -178,23 +194,22 @@ async def pay_invoice(
|
||||
If the payment is still in flight, we hope that some other process
|
||||
will regularly check for the payment.
|
||||
"""
|
||||
invoice = bolt11_decode(payment_request)
|
||||
try:
|
||||
invoice = bolt11_decode(payment_request)
|
||||
except Exception:
|
||||
raise InvoiceFailure("Bolt11 decoding failed.")
|
||||
|
||||
if not invoice.amount_msat or not invoice.amount_msat > 0:
|
||||
raise ValueError("Amountless invoices not supported.")
|
||||
raise InvoiceFailure("Amountless invoices not supported.")
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise ValueError("Amount in invoice is too high.")
|
||||
raise InvoiceFailure("Amount in invoice is too high.")
|
||||
|
||||
await check_wallet_limits(wallet_id, conn, invoice.amount_msat)
|
||||
|
||||
fee_reserve_msat = fee_reserve(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}"
|
||||
|
||||
if invoice.amount_msat == 0:
|
||||
raise ValueError("Amountless invoices not supported.")
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise ValueError("Amount in invoice is too high.")
|
||||
|
||||
_, extra = await calculate_fiat_amounts(
|
||||
invoice.amount_msat / 1000, wallet_id, extra=extra, conn=conn
|
||||
)
|
||||
@@ -228,6 +243,9 @@ async def pay_invoice(
|
||||
# (pending only)
|
||||
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
|
||||
if internal_checking_id:
|
||||
fee_reserve_total_msat = fee_reserve_total(
|
||||
invoice.amount_msat, internal=True
|
||||
)
|
||||
# perform additional checks on the internal payment
|
||||
# the payment hash is not enough to make sure that this is the same invoice
|
||||
internal_invoice = await get_standalone_payment(
|
||||
@@ -244,19 +262,22 @@ async def pay_invoice(
|
||||
# create a new payment from this wallet
|
||||
new_payment = await create_payment(
|
||||
checking_id=internal_id,
|
||||
fee=0,
|
||||
fee=0 + abs(fee_reserve_total_msat),
|
||||
pending=False,
|
||||
conn=conn,
|
||||
**payment_kwargs,
|
||||
)
|
||||
else:
|
||||
fee_reserve_total_msat = fee_reserve_total(
|
||||
invoice.amount_msat, internal=False
|
||||
)
|
||||
logger.debug(f"creating temporary payment with id {temp_id}")
|
||||
# create a temporary payment here so we can check if
|
||||
# the balance is enough in the next step
|
||||
try:
|
||||
new_payment = await create_payment(
|
||||
checking_id=temp_id,
|
||||
fee=-fee_reserve_msat,
|
||||
fee=-abs(fee_reserve_total_msat),
|
||||
conn=conn,
|
||||
**payment_kwargs,
|
||||
)
|
||||
@@ -270,14 +291,18 @@ async def pay_invoice(
|
||||
assert wallet, "Wallet for balancecheck could not be fetched"
|
||||
if wallet.balance_msat < 0:
|
||||
logger.debug("balance is too low, deleting temporary payment")
|
||||
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
|
||||
if (
|
||||
not internal_checking_id
|
||||
and wallet.balance_msat > -fee_reserve_total_msat
|
||||
):
|
||||
raise PaymentFailure(
|
||||
f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to"
|
||||
" cover potential routing fees."
|
||||
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
||||
" sat) to cover potential routing fees."
|
||||
)
|
||||
raise PermissionError("Insufficient balance.")
|
||||
|
||||
if internal_checking_id:
|
||||
service_fee_msat = service_fee(invoice.amount_msat, internal=True)
|
||||
logger.debug(f"marking temporary payment as not pending {internal_checking_id}")
|
||||
# mark the invoice from the other side as not pending anymore
|
||||
# so the other side only has access to his new money when we are sure
|
||||
@@ -294,6 +319,8 @@ async def pay_invoice(
|
||||
logger.debug(f"enqueuing internal invoice {internal_checking_id}")
|
||||
await internal_invoice_queue.put(internal_checking_id)
|
||||
else:
|
||||
fee_reserve_msat = fee_reserve(invoice.amount_msat, internal=False)
|
||||
service_fee_msat = service_fee(invoice.amount_msat, internal=False)
|
||||
logger.debug(f"backend: sending payment {temp_id}")
|
||||
# actually pay the external invoice
|
||||
WALLET = get_wallet_class()
|
||||
@@ -315,7 +342,10 @@ async def pay_invoice(
|
||||
await update_payment_details(
|
||||
checking_id=temp_id,
|
||||
pending=payment.ok is not True,
|
||||
fee=payment.fee_msat,
|
||||
fee=-(
|
||||
abs(payment.fee_msat if payment.fee_msat else 0)
|
||||
+ abs(service_fee_msat)
|
||||
),
|
||||
preimage=payment.preimage,
|
||||
new_checking_id=payment.checking_id,
|
||||
conn=conn,
|
||||
@@ -343,9 +373,73 @@ async def pay_invoice(
|
||||
f" database: {temp_id}"
|
||||
)
|
||||
|
||||
# credit service fee wallet
|
||||
if settings.lnbits_service_fee_wallet and service_fee_msat:
|
||||
new_payment = await create_payment(
|
||||
wallet_id=settings.lnbits_service_fee_wallet,
|
||||
fee=0,
|
||||
amount=abs(service_fee_msat),
|
||||
memo="Service fee",
|
||||
checking_id="service_fee" + temp_id,
|
||||
payment_request=payment_request,
|
||||
payment_hash=invoice.payment_hash,
|
||||
pending=False,
|
||||
)
|
||||
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,
|
||||
@@ -359,7 +453,8 @@ async def redeem_lnurl_withdraw(
|
||||
|
||||
res = {}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
lnurl = decode_lnurl(lnurl_request)
|
||||
r = await client.get(str(lnurl))
|
||||
res = r.json()
|
||||
@@ -393,7 +488,8 @@ async def redeem_lnurl_withdraw(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
try:
|
||||
await client.get(res["callback"], params=params)
|
||||
except Exception:
|
||||
@@ -456,7 +552,8 @@ async def perform_lnurlauth(
|
||||
|
||||
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
assert key.verifying_key, "LNURLauth verifying_key does not exist"
|
||||
r = await client.get(
|
||||
callback,
|
||||
@@ -485,10 +582,10 @@ async def check_transaction_status(
|
||||
wallet_id, payment_hash, conn=conn
|
||||
)
|
||||
if not payment:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
if not payment.pending:
|
||||
# note: before, we still checked the status of the payment again
|
||||
return PaymentStatus(True, fee_msat=payment.fee)
|
||||
return PaymentSuccessStatus(fee_msat=payment.fee)
|
||||
|
||||
status: PaymentStatus = await payment.check_status()
|
||||
return status
|
||||
@@ -496,12 +593,33 @@ async def check_transaction_status(
|
||||
|
||||
# WARN: this same value must be used for balance check and passed to
|
||||
# WALLET.pay_invoice(), it may cause a vulnerability if the values differ
|
||||
def fee_reserve(amount_msat: int) -> int:
|
||||
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
|
||||
if internal:
|
||||
return 0
|
||||
reserve_min = settings.lnbits_reserve_fee_min
|
||||
reserve_percent = settings.lnbits_reserve_fee_percent
|
||||
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
||||
|
||||
|
||||
def service_fee(amount_msat: int, internal: bool = False) -> int:
|
||||
service_fee_percent = settings.lnbits_service_fee
|
||||
fee_max = settings.lnbits_service_fee_max * 1000
|
||||
if settings.lnbits_service_fee_wallet:
|
||||
if internal and settings.lnbits_service_fee_ignore_internal:
|
||||
return 0
|
||||
fee_percentage = int(amount_msat / 100 * service_fee_percent)
|
||||
if fee_max > 0 and fee_percentage > fee_max:
|
||||
return fee_max
|
||||
else:
|
||||
return fee_percentage
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||
return fee_reserve(amount_msat, internal) + service_fee(amount_msat, internal)
|
||||
|
||||
|
||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
await websocketUpdater(
|
||||
wallet.id,
|
||||
@@ -561,6 +679,10 @@ async def check_admin_settings():
|
||||
):
|
||||
send_admin_user_to_saas()
|
||||
|
||||
account = await get_account(settings.super_user)
|
||||
if account and account.config and account.config.provider == "env":
|
||||
settings.first_install = True
|
||||
|
||||
logger.success(
|
||||
"✔️ Admin UI is enabled. run `poetry run lnbits-cli superuser` "
|
||||
"to get the superuser."
|
||||
@@ -592,11 +714,14 @@ async def check_webpush_settings():
|
||||
|
||||
def update_cached_settings(sets_dict: dict):
|
||||
for key, value in sets_dict.items():
|
||||
if key not in readonly_variables:
|
||||
try:
|
||||
setattr(settings, key, value)
|
||||
except Exception:
|
||||
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
||||
if key in readonly_variables:
|
||||
continue
|
||||
if key not in settings.dict().keys():
|
||||
continue
|
||||
try:
|
||||
setattr(settings, key, value)
|
||||
except Exception:
|
||||
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
||||
if "super_user" in sets_dict:
|
||||
setattr(settings, "super_user", sets_dict["super_user"])
|
||||
|
||||
@@ -606,7 +731,9 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
|
||||
if super_user:
|
||||
account = await get_account(super_user)
|
||||
if not account:
|
||||
account = await create_account(user_id=super_user)
|
||||
account = await create_account(
|
||||
user_id=super_user, user_config=UserConfig(provider="env")
|
||||
)
|
||||
if not account.wallets or len(account.wallets) == 0:
|
||||
await create_wallet(user_id=account.id)
|
||||
|
||||
|
||||
@@ -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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1,36 +0,0 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
disclaimerDialog: {
|
||||
show: false,
|
||||
data: {},
|
||||
description: ''
|
||||
},
|
||||
walletName: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formatDescription() {
|
||||
return LNbits.utils.convertMarkdown(this.description)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createWallet: function () {
|
||||
LNbits.api.createAccount(this.walletName).then(res => {
|
||||
window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id
|
||||
})
|
||||
},
|
||||
processing: function () {
|
||||
this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Processing...',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.description = SITE_DESCRIPTION
|
||||
}
|
||||
})
|
||||
+50
-66
@@ -8,8 +8,8 @@ from lnbits.core.crud import (
|
||||
get_balance_notify,
|
||||
get_wallet,
|
||||
get_webpush_subscriptions_for_user,
|
||||
mark_webhook_sent,
|
||||
)
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import (
|
||||
get_balance_delta,
|
||||
@@ -17,29 +17,16 @@ from lnbits.core.services import (
|
||||
switch_to_voidwallet,
|
||||
)
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.tasks import (
|
||||
SseListenersDict,
|
||||
create_permanent_task,
|
||||
create_task,
|
||||
register_invoice_listener,
|
||||
send_push_notification,
|
||||
)
|
||||
from lnbits.tasks import send_push_notification
|
||||
|
||||
api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
|
||||
"api_invoice_listeners"
|
||||
)
|
||||
|
||||
|
||||
def register_killswitch():
|
||||
"""
|
||||
Registers a killswitch which will check lnbits-status repository for a signal from
|
||||
LNbits and will switch to VoidWallet if the killswitch is triggered.
|
||||
"""
|
||||
logger.debug("Starting killswitch task")
|
||||
create_permanent_task(killswitch_task)
|
||||
api_invoice_listeners: Dict[str, asyncio.Queue] = {}
|
||||
|
||||
|
||||
async def killswitch_task():
|
||||
"""
|
||||
killswitch will check lnbits-status repository for a signal from
|
||||
LNbits and will switch to VoidWallet if the killswitch is triggered.
|
||||
"""
|
||||
while True:
|
||||
WALLET = get_wallet_class()
|
||||
if settings.lnbits_killswitch and WALLET.__class__.__name__ != "VoidWallet":
|
||||
@@ -54,7 +41,7 @@ async def killswitch_task():
|
||||
"Switching to VoidWallet. Killswitch triggered."
|
||||
)
|
||||
await switch_to_voidwallet()
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
except (httpx.RequestError, httpx.HTTPStatusError):
|
||||
logger.error(
|
||||
"Cannot fetch lnbits status manifest."
|
||||
f" {settings.lnbits_status_manifest}"
|
||||
@@ -62,17 +49,11 @@ async def killswitch_task():
|
||||
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
|
||||
|
||||
|
||||
async def register_watchdog():
|
||||
async def watchdog_task():
|
||||
"""
|
||||
Registers a watchdog which will check lnbits balance and nodebalance
|
||||
and will switch to VoidWallet if the watchdog delta is reached.
|
||||
"""
|
||||
# TODO: implement watchdog properly
|
||||
# logger.debug("Starting watchdog task")
|
||||
# create_permanent_task(watchdog_task)
|
||||
|
||||
|
||||
async def watchdog_task():
|
||||
while True:
|
||||
WALLET = get_wallet_class()
|
||||
if settings.lnbits_watchdog and WALLET.__class__.__name__ != "VoidWallet":
|
||||
@@ -87,46 +68,46 @@ async def watchdog_task():
|
||||
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
|
||||
|
||||
|
||||
def register_task_listeners():
|
||||
"""
|
||||
Registers an invoice listener queue for the core tasks. Incoming payments in this
|
||||
queue will eventually trigger the signals sent to all other extensions
|
||||
and fulfill other core tasks such as dispatching webhooks.
|
||||
"""
|
||||
invoice_paid_queue = asyncio.Queue(5)
|
||||
# we register invoice_paid_queue to receive all incoming invoices
|
||||
register_invoice_listener(invoice_paid_queue, "core/tasks.py")
|
||||
# register a worker that will react to invoices
|
||||
create_task(wait_for_paid_invoices(invoice_paid_queue))
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
"""
|
||||
This worker dispatches events to all extensions,
|
||||
dispatches webhooks and balance notifys.
|
||||
This task dispatches events to all api_invoice_listeners,
|
||||
webhooks, push notifications and balance notifications.
|
||||
"""
|
||||
while True:
|
||||
payment = await invoice_paid_queue.get()
|
||||
logger.trace("received invoice paid event")
|
||||
# send information to sse channel
|
||||
# dispatch api_invoice_listeners
|
||||
await dispatch_api_invoice_listeners(payment)
|
||||
# payment notification
|
||||
wallet = await get_wallet(payment.wallet_id)
|
||||
if wallet:
|
||||
await send_payment_notification(wallet, payment)
|
||||
# dispatch webhook
|
||||
if payment.webhook and not payment.webhook_status:
|
||||
await dispatch_webhook(payment)
|
||||
|
||||
# dispatch balance_notify
|
||||
url = await get_balance_notify(payment.wallet_id)
|
||||
if url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
try:
|
||||
r = await client.post(url, timeout=4)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
pass
|
||||
r.raise_for_status()
|
||||
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
status_code = exc.response.status_code
|
||||
await mark_webhook_sent(payment.payment_hash, status_code)
|
||||
logger.warning(
|
||||
f"balance_notify returned a bad status_code: {status_code} "
|
||||
f"while requesting {exc.request.url!r}."
|
||||
)
|
||||
logger.warning(exc)
|
||||
except httpx.RequestError as exc:
|
||||
await mark_webhook_sent(payment.payment_hash, -1)
|
||||
logger.warning(f"Could not send balance_notify to {url}")
|
||||
logger.warning(exc)
|
||||
|
||||
# dispatch push notification
|
||||
await send_payment_push_notification(payment)
|
||||
|
||||
|
||||
@@ -136,10 +117,12 @@ async def dispatch_api_invoice_listeners(payment: Payment):
|
||||
"""
|
||||
for chan_name, send_channel in api_invoice_listeners.items():
|
||||
try:
|
||||
logger.debug(f"sending invoice paid event to {chan_name}")
|
||||
logger.debug(f"api invoice listener: sending paid event to {chan_name}")
|
||||
send_channel.put_nowait(payment)
|
||||
except asyncio.QueueFull:
|
||||
logger.error(f"removing sse listener {send_channel}:{chan_name}")
|
||||
logger.error(
|
||||
f"api invoice listener: QueueFull, removing {send_channel}:{chan_name}"
|
||||
)
|
||||
api_invoice_listeners.pop(chan_name)
|
||||
|
||||
|
||||
@@ -150,25 +133,24 @@ async def dispatch_webhook(payment: Payment):
|
||||
logger.debug("sending webhook", payment.webhook)
|
||||
|
||||
if not payment.webhook:
|
||||
return await mark_webhook_sent(payment, -1)
|
||||
return await mark_webhook_sent(payment.payment_hash, -1)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
data = payment.dict()
|
||||
try:
|
||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET webhook_status = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(status, payment.payment_hash),
|
||||
)
|
||||
r.raise_for_status()
|
||||
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
|
||||
logger.warning(
|
||||
f"webhook returned a bad status_code: {exc.response.status_code} "
|
||||
f"while requesting {exc.request.url!r}."
|
||||
)
|
||||
except httpx.RequestError:
|
||||
await mark_webhook_sent(payment.payment_hash, -1)
|
||||
logger.warning(f"Could not send webhook to {payment.webhook}")
|
||||
|
||||
|
||||
async def send_payment_push_notification(payment: Payment):
|
||||
@@ -186,6 +168,8 @@ async def send_payment_push_notification(payment: Payment):
|
||||
body += f"\r\n{payment.memo}"
|
||||
|
||||
for subscription in subscriptions:
|
||||
# todo: review permissions when user-id-only not allowed
|
||||
# todo: replace all this logic with websockets?
|
||||
url = (
|
||||
f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
|
||||
)
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
<div class="col">
|
||||
<p>Funding Source Info</p>
|
||||
<ul>
|
||||
{%raw%}
|
||||
<li>Funding Source: {{settings.lnbits_backend_wallet_class}}</li>
|
||||
<li>
|
||||
Node Balance: {{(auditData.node_balance_msats /
|
||||
1000).toLocaleString()}} sats
|
||||
</li>
|
||||
<li>
|
||||
LNbits Balance: {{(auditData.lnbits_balance_msats /
|
||||
1000).toLocaleString()}} sats
|
||||
</li>
|
||||
<li>
|
||||
Reserve Percent: {{(auditData.node_balance_msats /
|
||||
auditData.lnbits_balance_msats * 100).toFixed(2)}} %
|
||||
</li>
|
||||
{%endraw%}
|
||||
<li
|
||||
v-text="'Funding Source: '+ settings.lnbits_backend_wallet_class"
|
||||
></li>
|
||||
<li
|
||||
v-text="'Node Balance: ' + (auditData.node_balance_msats /
|
||||
1000).toLocaleString() + ' sats'"
|
||||
></li>
|
||||
<li
|
||||
v-text="'LNbits Balance: ' + (auditData.lnbits_balance_msats /
|
||||
1000).toLocaleString() + ' sats'"
|
||||
></li>
|
||||
<li
|
||||
v-text="'Reserve Percent: ' + (auditData.node_balance_msats /
|
||||
auditData.lnbits_balance_msats * 100).toFixed(2) + ' %'"
|
||||
></li>
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,122 @@
|
||||
<q-tab-panel name="security">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none" v-text="$t('security_tools')"></h6>
|
||||
<h6 class="q-my-none q-mb-sm">Authentication</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.auth_token_expire_minutes"
|
||||
type="number"
|
||||
label="Token expire minutes"
|
||||
hint="Time in minutes until the token expires"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.auth_allowed_methods"
|
||||
multiple
|
||||
hint="Allowed authorization methods"
|
||||
label="Select authorization methods"
|
||||
:options="formData.auth_all_methods"
|
||||
></q-select>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section
|
||||
v-if="formData.auth_allowed_methods?.includes('google-auth')"
|
||||
class="q-pl-xl"
|
||||
>
|
||||
<strong class="q-my-none q-mb-sm">Google Auth</strong>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.google_client_id"
|
||||
label="Google Client ID"
|
||||
hint="Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.google_client_secret"
|
||||
type="password"
|
||||
label="Google Client Secret"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section
|
||||
v-if="formData.auth_allowed_methods?.includes('github-auth')"
|
||||
class="q-pl-xl"
|
||||
>
|
||||
<strong class="q-my-none q-mb-sm">GitHub Auth</strong>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.github_client_id"
|
||||
label="GitHub Client ID"
|
||||
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.github_client_secret"
|
||||
type="password"
|
||||
label="GitHub Client Secret"
|
||||
>
|
||||
</q-input>
|
||||
</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 />
|
||||
<h6 class="q-my-none" v-text="$t('security_tools')"></h6>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div v-if="serverlogEnabled" class="column" style="width: 100%">
|
||||
@@ -16,7 +131,7 @@
|
||||
style="padding: 10px; color: #fafafa; height: 320px"
|
||||
>
|
||||
<small v-for="log in logs"
|
||||
>{% raw %}{{ log }}{% endraw %}<br
|
||||
><span v-text="log"></span><br
|
||||
/></small>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
@@ -51,7 +166,6 @@
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="blocked_ip in formData.lnbits_blocked_ips"
|
||||
:key="blocked_ip"
|
||||
@@ -59,10 +173,8 @@
|
||||
@remove="removeBlockedIPs(blocked_ip)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ blocked_ip }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
v-text="blocked_ip"
|
||||
></q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
@@ -83,7 +195,6 @@
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="allowed_ip in formData.lnbits_allowed_ips"
|
||||
:key="allowed_ip"
|
||||
@@ -91,15 +202,14 @@
|
||||
@remove="removeAllowedIPs(allowed_ip)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ allowed_ip }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
v-text="allowed_ip"
|
||||
></q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-12">
|
||||
<p v-text="$t('rate_limiter')"></p>
|
||||
<div class="row q-col-gutter-md">
|
||||
@@ -121,6 +231,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 />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{% raw %}
|
||||
<q-banner v-if="updateAvailable" class="bg-primary text-white">
|
||||
<q-icon size="28px" name="update"></q-icon>
|
||||
|
||||
@@ -8,6 +7,7 @@
|
||||
class="q-btn"
|
||||
color="white"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/lnbits/lnbits/releases"
|
||||
v-text="$t('releases')"
|
||||
></a>
|
||||
@@ -29,9 +29,12 @@
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width> </q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props"
|
||||
>{{ col.label }}</q-th
|
||||
>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
@@ -50,14 +53,19 @@
|
||||
color="red"
|
||||
></q-icon>
|
||||
</q-td>
|
||||
<q-td auto-width key="date" :props="props">
|
||||
{{ formatDate(props.row.date) }}
|
||||
<q-td
|
||||
auto-width
|
||||
key="date"
|
||||
:props="props"
|
||||
v-text="formatDate(props.row.date)"
|
||||
>
|
||||
</q-td>
|
||||
<q-td key="message" :props="props"
|
||||
>{{ props.row.message }}
|
||||
<a
|
||||
><span v-text="props.row.message"></span
|
||||
><a
|
||||
v-if="props.row.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="props.row.link"
|
||||
v-text="$t('more')"
|
||||
></a
|
||||
@@ -67,4 +75,3 @@
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endraw %}
|
||||
|
||||
@@ -7,53 +7,18 @@
|
||||
<div class="col">
|
||||
<p>Server Info</p>
|
||||
<ul>
|
||||
{%raw%}
|
||||
<li v-if="settings.lnbits_data_folder">
|
||||
SQlite: {{settings.lnbits_data_folder}}
|
||||
</li>
|
||||
<li v-if="settings.lnbits_database_url">
|
||||
Postgres: {{settings.lnbits_database_url}}
|
||||
</li>
|
||||
{%endraw%}
|
||||
<li
|
||||
v-if="settings.lnbits_data_folder"
|
||||
v-text="'SQlite: ' + settings.lnbits_data_folder"
|
||||
></li>
|
||||
<li
|
||||
v-if="settings.lnbits_database_url"
|
||||
v-text="'Postgres: ' + settings.lnbits_database_url"
|
||||
></li>
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Service Fee</p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee"
|
||||
label="Service fee (%)"
|
||||
step="0.1"
|
||||
hint="Fee charged per tx (%)"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Miscellaneous</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Hide API</q-item-label>
|
||||
<q-item-label caption
|
||||
>Hides wallet api, extensions can choose to honor</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_hide_api"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Allowed currencies</p>
|
||||
@@ -93,8 +58,89 @@
|
||||
></q-select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Miscellaneous</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Hide API</q-item-label>
|
||||
<q-item-label caption
|
||||
>Hides wallet api, extensions can choose to honor</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_hide_api"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<h6 class="q-my-none">Service Fee</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Service Fee</p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee"
|
||||
label="Service fee (%)"
|
||||
step="0.1"
|
||||
hint="Fee charged per tx (%)"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Service fee max</p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee_max"
|
||||
label="Service fee max (sats)"
|
||||
hint="Max service fee to charge in (sats)"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Fee Wallet</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_service_fee_wallet"
|
||||
label="Fee wallet (wallet ID)"
|
||||
hint="Wallet ID to send funds to"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Disable Service Fee for Internal Payments</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Disable Fee</q-item-label>
|
||||
<q-item-label caption
|
||||
>Disable Service Fee for Internal Lightning
|
||||
Payments</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_service_fee_ignore_internal"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="q-my-none">Extensions</h6>
|
||||
<div>
|
||||
<p>Extension Sources</p>
|
||||
<q-input
|
||||
@@ -108,7 +154,6 @@
|
||||
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="manifestUrl in formData.lnbits_extensions_manifests"
|
||||
:key="manifestUrl"
|
||||
@@ -116,10 +161,8 @@
|
||||
@remove="removeExtensionsManifest(manifestUrl)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ manifestUrl }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
v-text="manifestUrl"
|
||||
></q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
@@ -1,62 +1,78 @@
|
||||
<q-tab-panel name="users">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none">User Management</h6>
|
||||
<br />
|
||||
<div>
|
||||
<p>Admin Users</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddAdmin"
|
||||
@keydown.enter="addAdminUser"
|
||||
type="text"
|
||||
label="User ID"
|
||||
hint="Users with admin privileges"
|
||||
>
|
||||
<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"
|
||||
removable
|
||||
@remove="removeAdminUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
<h6 class="q-my-none q-mb-sm">User Management</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p>Admin Users</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddAdmin"
|
||||
@keydown.enter="addAdminUser"
|
||||
type="text"
|
||||
label="User ID"
|
||||
hint="Users with admin privileges"
|
||||
>
|
||||
{{ user }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="user in formData.lnbits_admin_users"
|
||||
:key="user"
|
||||
removable
|
||||
@remove="removeAdminUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
<span v-text="user" />
|
||||
</q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div>
|
||||
<p>Allowed Users</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddUser"
|
||||
@keydown.enter="addAllowedUser"
|
||||
type="text"
|
||||
label="User ID"
|
||||
hint="Only these users can use LNbits"
|
||||
>
|
||||
<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"
|
||||
removable
|
||||
@remove="removeAllowedUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<p>Allowed Users</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddUser"
|
||||
@keydown.enter="addAllowedUser"
|
||||
type="text"
|
||||
label="User ID"
|
||||
hint="Only these users can use LNbits"
|
||||
>
|
||||
{{ user }}
|
||||
</q-chip>
|
||||
{% endraw %}
|
||||
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="user in formData.lnbits_allowed_users"
|
||||
:key="user"
|
||||
removable
|
||||
@remove="removeAllowedUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
<span v-text="user" />
|
||||
</q-chip>
|
||||
</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>
|
||||
<br />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
@click="updateSettings"
|
||||
:disabled="!checkChanges"
|
||||
>
|
||||
<q-tooltip v-if="checkChanges"
|
||||
>{%raw%}{{ $t('save_tooltip') }}{%endraw%}</q-tooltip
|
||||
>
|
||||
<q-tooltip v-if="checkChanges">
|
||||
<span v-text="$t('save_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
|
||||
<q-badge
|
||||
v-if="checkChanges"
|
||||
@@ -27,9 +27,9 @@
|
||||
color="primary"
|
||||
@click="restartServer"
|
||||
>
|
||||
<q-tooltip v-if="needsRestart"
|
||||
>{%raw%}{{ $t('restart_tooltip') }}{%endraw%}</q-tooltip
|
||||
>
|
||||
<q-tooltip v-if="needsRestart">
|
||||
<span v-text="$t('restart_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
|
||||
<q-badge
|
||||
v-if="needsRestart"
|
||||
@@ -46,7 +46,9 @@
|
||||
color="primary"
|
||||
@click="topUpDialog.show = true"
|
||||
>
|
||||
<q-tooltip>{%raw%}{{ $t('add_funds_tooltip') }}{%endraw%}</q-tooltip>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('add_funds_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
|
||||
@@ -59,7 +61,9 @@
|
||||
@click="deleteSettings"
|
||||
class="float-right"
|
||||
>
|
||||
<q-tooltip>{%raw%}{{ $t('reset_defaults_tooltip') }}{%endraw%}</q-tooltip>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('reset_defaults_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,5 +168,5 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
:label="$t('api_docs')"
|
||||
icon="vpn_key"
|
||||
:label="$t('api_keys_api_docs')"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card-section>
|
||||
<strong>Node URL: </strong><em v-text="origin"></em><br />
|
||||
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br />
|
||||
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br />
|
||||
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em>
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
{% extends "base.html" %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/account.js') }}"></script>
|
||||
{% endblock %} {% block page %}
|
||||
|
||||
<div class="row q-col-gutter-md">
|
||||
<div v-if="user" class="col-12 col-md-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-tabs v-model="tab" align="justify">
|
||||
<q-tab
|
||||
name="user"
|
||||
:label="$t('account_settings')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="theme"
|
||||
:label="$t('look_and_feel')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
</q-tabs>
|
||||
<q-tab-panels v-model="tab">
|
||||
<q-tab-panel name="user">
|
||||
<div v-if="passwordData.show">
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4 class="q-my-none">
|
||||
<span v-text="$t('password_config')"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-img
|
||||
v-if="user.config.picture"
|
||||
style="max-width: 100px"
|
||||
:src="user.config.picture"
|
||||
class="float-right"
|
||||
></q-img>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-if="user.has_password"
|
||||
v-model="passwordData.oldPassword"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
label="Old Password"
|
||||
filled
|
||||
dense
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
></q-input>
|
||||
<q-input
|
||||
v-model="passwordData.newPassword"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:label="$t('password')"
|
||||
filled
|
||||
dense
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
></q-input>
|
||||
<q-input
|
||||
v-model="passwordData.newPasswordRepeat"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:label="$t('password_repeat')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
></q-input>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section class="q-pb-lg">
|
||||
<q-btn
|
||||
@click="updatePassword"
|
||||
:disable="(!passwordData.newPassword || !passwordData.newPasswordRepeat) || passwordData.newPassword !== passwordData.newPasswordRepeat"
|
||||
unelevated
|
||||
color="primary"
|
||||
:label="$t('change_password')"
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@click="passwordData.show = false"
|
||||
:label="$t('back')"
|
||||
outline
|
||||
unelevated
|
||||
color="grey"
|
||||
class="float-right"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-img
|
||||
v-if="user.config.picture"
|
||||
style="max-width: 100px"
|
||||
:src="user.config.picture"
|
||||
class="float-right"
|
||||
></q-img>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="user.id"
|
||||
:label="$t('user_id')"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
:type="showUserId ? 'text': 'password'"
|
||||
class="q-mb-md"
|
||||
><q-btn
|
||||
@click="showUserId = !showUserId"
|
||||
dense
|
||||
flat
|
||||
:icon="showUserId ? 'visibility_off' : 'visibility'"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="user.username"
|
||||
:label="$t('username')"
|
||||
filled
|
||||
dense
|
||||
:readonly="hasUsername"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="user.email"
|
||||
:label="$t('email')"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<div v-if="!user.email" class="row"></div>
|
||||
<div v-if="!user.email" class="row">
|
||||
{% if "google-auth" in LNBITS_AUTH_METHODS or
|
||||
"github-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col q-pa-sm text-h6">
|
||||
<span v-text="$t('verify_email')"></span>:
|
||||
</div>
|
||||
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col q-pa-sm">
|
||||
<q-btn
|
||||
:href="`/api/v1/auth/google?user_id=${user.id}`"
|
||||
type="a"
|
||||
outline
|
||||
no-caps
|
||||
rounded
|
||||
color="grey"
|
||||
class="full-width"
|
||||
>
|
||||
<q-avatar size="32px" class="q-mr-md">
|
||||
<q-img
|
||||
:src="'{{ static_url_for('static', 'images/google-logo.png') }}'"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
<div>Google</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col q-pa-sm">
|
||||
<q-btn
|
||||
:href="`/api/v1/auth/github?user_id=${user.id}`"
|
||||
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/github-logo.png') }}'"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
<div>GitHub</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="user.config">
|
||||
<q-input
|
||||
v-model="user.config.first_name"
|
||||
:label="$t('first_name')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="user.config.last_name"
|
||||
:label="$t('last_name')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="user.config.provider"
|
||||
:label="$t('auth_provider')"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="user.config.picture"
|
||||
:label="$t('picture')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section>
|
||||
<q-btn @click="updateAccount" unelevated color="primary">
|
||||
<span v-text="$t('update_account')"></span>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@click="showChangePassword()"
|
||||
:label="user.has_password ? $t('change_password'): $t('set_password')"
|
||||
outline
|
||||
unelevated
|
||||
color="grey"
|
||||
class="float-right"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="theme">
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4"><span v-text="$t('language')"></span></div>
|
||||
<div class="col-8">
|
||||
<q-btn-dropdown
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
icon="language"
|
||||
class="q-pl-md"
|
||||
>
|
||||
<q-list v-for="(lang, index) in g.langs" :key="index">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
:active="activeLanguage(lang.value)"
|
||||
@click="changeLanguage(lang.value)"
|
||||
><q-item-section>
|
||||
<q-item-label
|
||||
v-text="lang.display ?? lang.value.toUpperCase()"
|
||||
></q-item-label>
|
||||
<q-tooltip
|
||||
><span v-text="lang.label"></span
|
||||
></q-tooltip>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('color_scheme')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('classic')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('classic')"
|
||||
icon="circle"
|
||||
color="deep-purple"
|
||||
size="md"
|
||||
><q-tooltip>classic</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('bitcoin')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('bitcoin')"
|
||||
icon="circle"
|
||||
color="orange"
|
||||
size="md"
|
||||
><q-tooltip>bitcoin</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('mint')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('mint')"
|
||||
icon="circle"
|
||||
color="green"
|
||||
size="md"
|
||||
><q-tooltip>mint</q-tooltip> </q-btn
|
||||
><q-btn
|
||||
v-if="g.allowedThemes.includes('autumn')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('autumn')"
|
||||
icon="circle"
|
||||
color="brown"
|
||||
size="md"
|
||||
><q-tooltip>autumn</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('monochrome')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('monochrome')"
|
||||
icon="circle"
|
||||
color="grey"
|
||||
size="md"
|
||||
><q-tooltip>monochrome</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('salvador')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('salvador')"
|
||||
icon="circle"
|
||||
color="blue-10"
|
||||
size="md"
|
||||
><q-tooltip>elSalvador</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('freedom')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('freedom')"
|
||||
icon="circle"
|
||||
color="pink-13"
|
||||
size="md"
|
||||
><q-tooltip>freedom</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('cyber')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('cyber')"
|
||||
icon="circle"
|
||||
color="light-green-9"
|
||||
size="md"
|
||||
><q-tooltip>cyber</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="g.allowedThemes.includes('flamingo')"
|
||||
dense
|
||||
flat
|
||||
@click="changeColor('flamingo')"
|
||||
icon="circle"
|
||||
color="pink-3"
|
||||
size="md"
|
||||
><q-tooltip>flamingo</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('toggle_darkmode')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
round
|
||||
@click="toggleDarkMode"
|
||||
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
|
||||
size="sm"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('toggle_darkmode')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">Notifications</div>
|
||||
<div class="col-8">
|
||||
<lnbits-notifications-btn
|
||||
v-if="g.user"
|
||||
pubkey="{{ WEBPUSH_PUBKEY }}"
|
||||
></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>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div v-else class="col-12 col-md-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h4 class="q-my-none"><span v-text="$t('account')"></span></h4>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,7 @@
|
||||
%} {{ window_vars(user, extensions) }}{% block page %}
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-sm-9 col-xs-12">
|
||||
<p class="text-h4 gt-sm">{%raw%}{{ $t('extensions') }}{%endraw%}</p>
|
||||
<p class="text-h4 gt-sm" v-text="$t('extensions')"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-xs-12 q-ml-auto">
|
||||
@@ -43,9 +43,10 @@
|
||||
:label="$t('featured')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
<i v-if="!g.user.admin && tab != 'installed'"
|
||||
>{%raw%}{{ $t('only_admins_can_install') }}{%endraw%}</i
|
||||
>
|
||||
<i
|
||||
v-if="!g.user.admin && tab != 'installed'"
|
||||
v-text="$t('only_admins_can_install')"
|
||||
></i>
|
||||
</q-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,45 +90,42 @@
|
||||
color="green"
|
||||
class="float-right"
|
||||
>
|
||||
<small>{%raw%}{{ $t('new_version') }}{%endraw%}</small>
|
||||
<small v-text="$t('new_version')"></small>
|
||||
<q-tooltip
|
||||
><span v-text="extension.latestRelease.version"></span
|
||||
></q-tooltip>
|
||||
</q-badge>
|
||||
{% raw %}
|
||||
<div class="text-h5 gt-sm q-mt-sm q-mb-xs gt-sm">
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-h5 gt-sm q-mt-sm q-mb-xs gt-sm"
|
||||
v-text="extension.name"
|
||||
></div>
|
||||
<div
|
||||
class="text-h5 gt-sm q-mt-sm q-mb-xs lt-md"
|
||||
style="min-height: 60px"
|
||||
>
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
v-text="extension.name"
|
||||
></div>
|
||||
<div
|
||||
class="text-subtitle2 gt-sm"
|
||||
style="font-size: 11px; height: 34px"
|
||||
>
|
||||
{{ extension.shortDescription ||
|
||||
extension.installedRelease?.description }}
|
||||
</div>
|
||||
<div class="text-subtitle1 lt-md q-mt-sm q-mb-xs">
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
v-text="extension.shortDescription || extension.installedRelease?.description"
|
||||
></div>
|
||||
<div
|
||||
class="text-subtitle1 lt-md q-mt-sm q-mb-xs"
|
||||
v-text="extension.name"
|
||||
></div>
|
||||
<div
|
||||
class="text-subtitle2 lt-md"
|
||||
style="font-size: 9px; height: 34px"
|
||||
>
|
||||
{{ extension.shortDescription }}
|
||||
</div>
|
||||
{% endraw %}
|
||||
v-text="extension.shortDescription"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-pt-sm">
|
||||
<div class="col">
|
||||
<small v-if="extension.dependencies?.length"
|
||||
>{%raw%}{{ $t('extension_depends_on') }}{%endraw%}</small
|
||||
>
|
||||
<small
|
||||
v-if="extension.dependencies?.length"
|
||||
v-text="$t('extension_depends_on')"
|
||||
></small>
|
||||
<small v-else> </small>
|
||||
<q-badge
|
||||
v-for="dep in extension.dependencies"
|
||||
@@ -148,20 +146,18 @@
|
||||
size="1.5em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
><q-tooltip
|
||||
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
|
||||
></q-rating
|
||||
>
|
||||
><q-tooltip>
|
||||
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
||||
></q-rating>
|
||||
<q-rating
|
||||
v-model="maxStars"
|
||||
class="lt-md"
|
||||
size="1.5em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
><q-tooltip
|
||||
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
|
||||
></q-rating
|
||||
>
|
||||
><q-tooltip>
|
||||
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
||||
></q-rating>
|
||||
<q-toggle
|
||||
v-if="extension.isAvailable && extension.isInstalled && g.user.admin"
|
||||
:label="extension.isActive ? $t('activated'): $t('deactivated') "
|
||||
@@ -169,11 +165,11 @@
|
||||
style="max-height: 21px"
|
||||
v-model="extension.isActive"
|
||||
@input="toggleExtension(extension)"
|
||||
><q-tooltip
|
||||
>{%raw%}{{ $t('activate_extension_details')
|
||||
}}{%endraw%}</q-tooltip
|
||||
></q-toggle
|
||||
>
|
||||
><q-tooltip>
|
||||
<span
|
||||
v-text="$t('activate_extension_details')"
|
||||
></span> </q-tooltip
|
||||
></q-toggle>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
@@ -185,9 +181,9 @@
|
||||
flat
|
||||
color="primary"
|
||||
type="a"
|
||||
:href="[extension.id, '/?usr=', g.user.id].join('')"
|
||||
>{%raw%}{{ $t('open') }}{%endraw%}</q-btn
|
||||
>
|
||||
:href="extension.id + '/'"
|
||||
:label="$t('open')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
||||
flat
|
||||
@@ -195,12 +191,13 @@
|
||||
type="a"
|
||||
:href="['{{
|
||||
url_for('install.extensions')
|
||||
}}', '?usr=', g.user.id, '&disable=', extension.id].join('')"
|
||||
}}', '?disable=', extension.id].join('')"
|
||||
:label="$t('disable')"
|
||||
></q-btn>
|
||||
<q-badge
|
||||
v-if="extension.isAdminOnly && !user.admin"
|
||||
v-text="$t('admin_only')"
|
||||
>
|
||||
{%raw%}{{ $t('disable') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-badge v-if="extension.isAdminOnly && !user.admin">
|
||||
{%raw%}{{ $t('admin_only') }}{%endraw%}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
|
||||
@@ -209,9 +206,9 @@
|
||||
type="a"
|
||||
:href="['{{
|
||||
url_for('install.extensions')
|
||||
}}', '?usr=', g.user.id, '&enable=', extension.id].join('')"
|
||||
}}', '?enable=', extension.id].join('')"
|
||||
:label="$t('enable')"
|
||||
>
|
||||
{%raw%}{{ $t('enable') }}{%endraw%}
|
||||
<q-tooltip>
|
||||
<span v-text="$t('enable_extension_details')">
|
||||
</span> </q-tooltip
|
||||
@@ -222,12 +219,11 @@
|
||||
flat
|
||||
color="primary"
|
||||
v-if="g.user.admin"
|
||||
>
|
||||
{%raw%}{{ $t('manage') }}{%endraw%}<q-tooltip
|
||||
>{%raw%}{{ $t('manage_extension_details')
|
||||
}}{%endraw%}</q-tooltip
|
||||
></q-btn
|
||||
>
|
||||
:label="$t('manage')"
|
||||
><q-tooltip
|
||||
><span v-text="$t('manage_extension_details')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
@@ -240,11 +236,10 @@
|
||||
class="float-right"
|
||||
>
|
||||
<q-badge>
|
||||
{% raw %}{{ extension.installedRelease.version }}{% endraw
|
||||
%}<q-tooltip
|
||||
>{%raw%}{{ $t('extension_installed_version')
|
||||
}}{%endraw%}</q-tooltip
|
||||
>
|
||||
<span v-text="extension.installedRelease.version"></span>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('extension_installed_version')"></span>
|
||||
</q-tooltip>
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,10 +248,10 @@
|
||||
</div>
|
||||
<q-dialog v-model="showUninstallDialog">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
|
||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||
<p>
|
||||
{%raw%}{{ $t('extension_uninstall_warning') }}{%endraw%} <br />
|
||||
{%raw%}{{ $t('confirm_continue') }}{%endraw%}
|
||||
<span v-text="$t('extension_uninstall_warning')"></span><br />
|
||||
<span v-text="$t('confirm_continue')"></span>
|
||||
</p>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
@@ -265,32 +260,39 @@
|
||||
value="false"
|
||||
label="Cleanup database tables"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
{%raw%}{{ $t('extension_db_drop_info') }}{%endraw%}
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
><span v-text="$t('extension_db_drop_info')"></span>
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="uninstallExtension()"
|
||||
>{%raw%}{{ $t('uninstall_confirm') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="uninstallExtension()"
|
||||
v-text="$t('uninstall_confirm')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showDropDbDialog">
|
||||
<q-card v-if="selectedExtension" class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
|
||||
<p>{%raw%}{{ $t('extension_db_drop_warning') }}{%endraw%} <br /></p>
|
||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
|
||||
<q-input
|
||||
v-model="dropDbExtensionId"
|
||||
:label="selectedExtension.id"
|
||||
></q-input>
|
||||
<br />
|
||||
<p>{%raw%}{{ $t('confirm_continue') }}{%endraw%}</p>
|
||||
<p v-text="$t('confirm_continue')"></p>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@@ -298,17 +300,58 @@
|
||||
outline
|
||||
color="red"
|
||||
@click="dropExtensionDb()"
|
||||
>{%raw%}{{ $t('confirm') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
|
||||
>
|
||||
v-text="$t('confirm')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showUpgradeDialog">
|
||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div v-if="selectedRelease.paymentRequest">
|
||||
<a :href="'lightning:' + selectedRelease.paymentRequest">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
v-if="selectedRelease.paymentRequest"
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(selectedRelease.paymentRequest)"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="float-right q-ml-lg"
|
||||
v-text="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6" v-text="selectedExtension?.name"></div>
|
||||
</q-card-section>
|
||||
@@ -338,7 +381,7 @@
|
||||
<q-item-section>
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
{%raw%}{{ $t('repository') }}{%endraw%}
|
||||
<span v-text="$t('repository')"></span>
|
||||
<br />
|
||||
<small v-text="repoName"></small>
|
||||
<q-tooltip
|
||||
@@ -377,30 +420,116 @@
|
||||
color="pink"
|
||||
size="70px"
|
||||
></q-icon>
|
||||
Cannot get the release details.
|
||||
<span v-text="$t('release_details_error')"></span>
|
||||
</div>
|
||||
<q-card v-else>
|
||||
<q-card-section v-if="release.is_version_compatible">
|
||||
<q-btn
|
||||
v-if="!release.isInstalled"
|
||||
@click="installExtension(release)"
|
||||
color="primary unelevated mt-lg pt-lg"
|
||||
>{%raw%}{{ $t('install') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-else @click="showUninstall()" flat color="red">
|
||||
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
||||
<span
|
||||
v-if="release.requiresPayment && !release.paid_sats"
|
||||
v-text="$t('extension_cost', {cost: release.cost_sats})"
|
||||
class="q-mb-lg"
|
||||
></span>
|
||||
<span
|
||||
v-if="release.requiresPayment && release.paid_sats"
|
||||
class="q-mb-lg"
|
||||
v-text="$t('extension_paid_sats', {paid_sats: release.paid_sats})"
|
||||
></span>
|
||||
<div
|
||||
v-if="!release.requiresPayment || (release.requiresPayment && release.paid_sats)"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!release.isInstalled"
|
||||
@click="installExtension(release)"
|
||||
color="primary unelevated mt-lg pt-lg"
|
||||
:label="$t('install')"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="release.requiresPayment && !release.paid_sats">
|
||||
<div v-if="!release.payment_hash">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="release.paidAmount"
|
||||
type="number"
|
||||
:min="release.cost_sats"
|
||||
suffix="sat"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
</q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="release.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
</q-select>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="payAndInstall(release)"
|
||||
:disabled="!release.wallet"
|
||||
class="q-mt-sm"
|
||||
:label="$t('pay_from_wallet')"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="showQRCode(release)"
|
||||
class="q-mt-sm float-right"
|
||||
:label="$t('show_qr')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<br />
|
||||
<span
|
||||
class="q-mb-lg q-mt-lg"
|
||||
v-text="'There is a previous pending invoice for this release.'"
|
||||
></span>
|
||||
<br />
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="installExtension(release)"
|
||||
color="primary"
|
||||
class="q-mt-sm"
|
||||
:label="$t('retry_install')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="clearHangingInvoice(release)"
|
||||
color="primary"
|
||||
class="q-mt-sm float-right"
|
||||
:label="$t('new_payment')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="release.isInstalled"
|
||||
@click="showUninstall()"
|
||||
:label="$t('uninstall')"
|
||||
flat
|
||||
color="red"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="release.html_url"
|
||||
class="text-secondary float-right"
|
||||
:href="release.html_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="color: inherit"
|
||||
>{%raw%}{{ $t('release_notes') }}{%endraw%}</a
|
||||
>
|
||||
v-text="$t('release_notes')"
|
||||
></a>
|
||||
</q-card-section>
|
||||
<q-card-section v-else>
|
||||
{%raw%}{{ $t('extension_min_lnbits_version') }}{%endraw%}
|
||||
<span v-text="$t('extension_min_lnbits_version')"></span>
|
||||
<strong>
|
||||
<span v-text="release.min_lnbits_version"></span>
|
||||
</strong>
|
||||
@@ -408,8 +537,11 @@
|
||||
<q-card v-if="release.warning">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<q-badge color="yellow" text-color="black">
|
||||
{%raw%}{{ $t('warning') }}{%endraw%}
|
||||
<q-badge
|
||||
color="yellow"
|
||||
text-color="black"
|
||||
v-text="$t('warning')"
|
||||
>
|
||||
</q-badge>
|
||||
</div>
|
||||
<div class="text-subtitle2">
|
||||
@@ -432,9 +564,8 @@
|
||||
@click="showUninstall()"
|
||||
flat
|
||||
color="red"
|
||||
>
|
||||
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
||||
>
|
||||
v-text="$t('uninstall')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else-if="selectedExtension?.hasDatabaseTables"
|
||||
@click="showDropDb()"
|
||||
@@ -442,9 +573,13 @@
|
||||
color="red"
|
||||
:label="$t('drop_db')"
|
||||
></q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
|
||||
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -464,8 +599,10 @@
|
||||
dropDbExtensionId: '',
|
||||
selectedExtension: null,
|
||||
selectedExtensionRepos: null,
|
||||
selectedRelease: null,
|
||||
uninstallAndDropDb: false,
|
||||
maxStars: 5,
|
||||
paylinkWebsocket: null,
|
||||
user: null
|
||||
}
|
||||
},
|
||||
@@ -504,19 +641,29 @@
|
||||
.filter(extensionNameContains(term))
|
||||
this.tab = tab
|
||||
},
|
||||
|
||||
installExtension: async function (release) {
|
||||
// no longer required to check if the invoice was paid
|
||||
// the install logic has been triggered one way or another
|
||||
this.unsubscribeFromPaylinkWs()
|
||||
|
||||
const extension = this.selectedExtension
|
||||
extension.inProgress = true
|
||||
this.showUpgradeDialog = false
|
||||
release.payment_hash =
|
||||
release.payment_hash || this.getPaylinkHash(release.pay_link)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
`/api/v1/extension?usr=${this.g.user.id}`,
|
||||
`/api/v1/extension`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
ext_id: extension.id,
|
||||
archive: release.archive,
|
||||
source_repo: release.source_repo
|
||||
source_repo: release.source_repo,
|
||||
payment_hash: release.payment_hash,
|
||||
version: release.version
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
@@ -530,8 +677,9 @@
|
||||
this.tab = 'installed'
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
console.warn(err)
|
||||
extension.inProgress = false
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
uninstallExtension: async function () {
|
||||
@@ -542,7 +690,7 @@
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
`/api/v1/extension/${extension.id}?usr=${this.g.user.id}`,
|
||||
`/api/v1/extension/${extension.id}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
@@ -576,7 +724,7 @@
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
`/api/v1/extension/${extension.id}/db?usr=${this.g.user.id}`,
|
||||
`/api/v1/extension/${extension.id}/db`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
@@ -598,9 +746,8 @@
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
"{{ url_for('install.extensions') }}?usr=" +
|
||||
this.g.user.id +
|
||||
'&' +
|
||||
"{{ url_for('install.extensions') }}" +
|
||||
'?' +
|
||||
action +
|
||||
'=' +
|
||||
extension.id
|
||||
@@ -624,12 +771,14 @@
|
||||
|
||||
showUpgrade: async function (extension) {
|
||||
this.selectedExtension = extension
|
||||
this.showUpgradeDialog = true
|
||||
this.selectedRelease = null
|
||||
this.selectedExtensionRepos = null
|
||||
this.showUpgradeDialog = true
|
||||
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/${extension.id}/releases?usr=${this.g.user.id}`,
|
||||
`/api/v1/extension/${extension.id}/releases`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
|
||||
@@ -649,6 +798,12 @@
|
||||
if (release.isInstalled) {
|
||||
repos[release.source_repo].isInstalled = true
|
||||
}
|
||||
if (release.pay_link) {
|
||||
release.requiresPayment = true
|
||||
release.paidAmount = release.cost_sats
|
||||
release.payment_hash = this.getPaylinkHash(release.pay_link)
|
||||
}
|
||||
|
||||
repos[release.source_repo].releases.push(release)
|
||||
return repos
|
||||
}, {})
|
||||
@@ -657,6 +812,122 @@
|
||||
extension.inProgress = false
|
||||
}
|
||||
},
|
||||
|
||||
async payAndInstall(release) {
|
||||
try {
|
||||
this.selectedExtension.inProgress = true
|
||||
this.showUpgradeDialog = false
|
||||
const paymentInfo = await this.requestPayment(
|
||||
this.selectedExtension.id,
|
||||
release
|
||||
)
|
||||
this.rememberPaylinkHash(release.pay_link, paymentInfo.payment_hash)
|
||||
const wallet = this.g.user.wallets.find(w => w.id === release.wallet)
|
||||
const {data} = await LNbits.api.payInvoice(
|
||||
wallet,
|
||||
paymentInfo.payment_request
|
||||
)
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
|
||||
await this.installExtension(release)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
LNbits.utils.notifyApiError(err)
|
||||
} finally {
|
||||
this.selectedExtension.inProgress = false
|
||||
}
|
||||
},
|
||||
async showQRCode(release) {
|
||||
this.selectedRelease = release
|
||||
|
||||
try {
|
||||
const data = await this.requestPayment(
|
||||
this.selectedExtension.id,
|
||||
release
|
||||
)
|
||||
|
||||
this.selectedRelease.paymentRequest = data.payment_request
|
||||
this.selectedRelease.payment_hash = data.payment_hash
|
||||
this.selectedRelease = _.clone(this.selectedRelease)
|
||||
this.rememberPaylinkHash(
|
||||
this.selectedRelease.pay_link,
|
||||
this.selectedRelease.payment_hash
|
||||
)
|
||||
|
||||
this.subscribeToPaylinkWs(
|
||||
this.selectedRelease.pay_link,
|
||||
data.payment_hash
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async requestPayment(extId, release) {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/invoice`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
ext_id: extId,
|
||||
archive: release.archive,
|
||||
source_repo: release.source_repo,
|
||||
cost_sats: release.paidAmount,
|
||||
version: release.version
|
||||
}
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
||||
clearHangingInvoice(release) {
|
||||
this.forgetPaylinkHash(release.pay_link)
|
||||
release.payment_hash = null
|
||||
},
|
||||
|
||||
rememberPaylinkHash(pay_link, payment_hash) {
|
||||
this.$q.localStorage.set(
|
||||
`lnbits.extensions.paylink.${pay_link}`,
|
||||
payment_hash
|
||||
)
|
||||
},
|
||||
getPaylinkHash(pay_link) {
|
||||
return this.$q.localStorage.getItem(
|
||||
`lnbits.extensions.paylink.${pay_link}`
|
||||
)
|
||||
},
|
||||
forgetPaylinkHash(pay_link) {
|
||||
this.$q.localStorage.remove(`lnbits.extensions.paylink.${pay_link}`)
|
||||
},
|
||||
subscribeToPaylinkWs(pay_link, payment_hash) {
|
||||
const url = new URL(`${pay_link}/${payment_hash}`)
|
||||
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
|
||||
this.paylinkWebsocket = new WebSocket(url)
|
||||
this.paylinkWebsocket.addEventListener('message', async ({data}) => {
|
||||
const resp = JSON.parse(data)
|
||||
if (resp.paid) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Invoice Paid!'
|
||||
})
|
||||
this.installExtension(this.selectedRelease)
|
||||
} else {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Invoice tracking lost!'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
unsubscribeFromPaylinkWs() {
|
||||
try {
|
||||
this.paylinkWebsocket && this.paylinkWebsocket.close()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
|
||||
hasNewVersion: function (extension) {
|
||||
if (extension.installedRelease && extension.latestRelease) {
|
||||
return (
|
||||
@@ -694,7 +965,7 @@
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/release/${org}/${repo}/${release.version}?usr=${this.g.user.id}`,
|
||||
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
release.loaded = true
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center main">
|
||||
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section class="grid">
|
||||
<div>
|
||||
<h6 class="q-my-none text-center">
|
||||
<strong>Welcome to LNbits</strong>
|
||||
<p>Set up the Superuser account below.</p>
|
||||
</h6>
|
||||
<br />
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="loginData.username"
|
||||
:label="$t('username')"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
v-model.trim="loginData.password"
|
||||
:type="loginData.isPwd ? 'password' : 'text'"
|
||||
autocomplete="off"
|
||||
:label="$t('password')"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="loginData.isPwd = !loginData.isPwd"
|
||||
/> </template
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
v-model.trim="loginData.passwordRepeat"
|
||||
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
||||
autocomplete="off"
|
||||
:label="$t('password_repeat')"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || 'Passwords_dont_match']"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
||||
/> </template
|
||||
></q-input>
|
||||
<q-btn
|
||||
@click="setPassword()"
|
||||
unelevated
|
||||
color="primary"
|
||||
:label="$t('login')"
|
||||
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
|
||||
></q-btn>
|
||||
</q-form>
|
||||
</div>
|
||||
<div class="hero-wrapper">
|
||||
<div class="hero q-mx-auto"></div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.grid {
|
||||
display: block;
|
||||
}
|
||||
.hero-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.hero {
|
||||
display: block;
|
||||
height: 100%;
|
||||
max-width: 250px;
|
||||
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
.hero-wrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
loginData: {
|
||||
isPwd: true,
|
||||
isPwdRepeat: true,
|
||||
username: '',
|
||||
password: '',
|
||||
passwordRepeat: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.hasAdminUI = '{{ LNBITS_ADMIN_UI | tojson}}'
|
||||
},
|
||||
computed: {
|
||||
checkPasswordsMatch() {
|
||||
return this.loginData.password !== this.loginData.passwordRepeat
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async setPassword() {
|
||||
try {
|
||||
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
|
||||
username: this.loginData.username,
|
||||
password: this.loginData.password,
|
||||
password_repeat: this.loginData.passwordRepeat
|
||||
})
|
||||
|
||||
window.location.href = '/admin'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,78 +1,343 @@
|
||||
{% extends "public.html" %} {% block scripts %}
|
||||
<script src="/core/static/js/index.js"></script>
|
||||
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
||||
{% endblock %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
{% if lnurl or LNBITS_NEW_ACCOUNTS_ALLOWED %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
{% if lnurl %}
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="processing"
|
||||
type="a"
|
||||
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
|
||||
v-text="$t('press_to_claim')"
|
||||
></q-btn>
|
||||
{% elif LNBITS_NEW_ACCOUNTS_ALLOWED %}
|
||||
<q-form @submit="createWallet" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="walletName"
|
||||
:label='$t("name_your_wallet", { name: "{{ SITE_TITLE }} *" })'
|
||||
></q-input>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="walletName == ''"
|
||||
type="submit"
|
||||
:label="$t('add_wallet')"
|
||||
></q-btn>
|
||||
</q-form>
|
||||
{% endif %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endif %}
|
||||
<div
|
||||
v-if="isUserAuthorized"
|
||||
class="col-12 col-md-6 col-lg-6 q-gutter-y-md"
|
||||
></div>
|
||||
<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" 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)%}
|
||||
<div class="row q-mt-xl">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="processing"
|
||||
type="a"
|
||||
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
|
||||
v-text="$t('press_to_claim')"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{%else%} {% endif %}
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
|
||||
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
|
||||
<div v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
<p v-text="$t('lnbits_description')"></p>
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://github.com/lnbits/lnbits"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
:label="$t('view_github')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
:label="$t('donate')"
|
||||
></q-btn>
|
||||
<div class="row q-mt-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>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-html="formatDescription"></div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-badge>
|
||||
<q-card class="shadow-12">
|
||||
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<span v-text="$t('instant_access_question')"></span>
|
||||
<br />
|
||||
<q-badge
|
||||
@click="showLogin('user-id-only')"
|
||||
color="primary"
|
||||
class="cursor-pointer"
|
||||
rounded
|
||||
>
|
||||
<strong>
|
||||
<q-icon name="account_circle" size="xs"></q-icon>
|
||||
<span v-text="$t('login_with_user_id')"></span> </strong
|
||||
></q-badge>
|
||||
{% if LNBITS_NEW_ACCOUNTS_ALLOWED %}
|
||||
<span v-text="$t('or')"></span>
|
||||
<q-badge
|
||||
@click="showRegister('user-id-only')"
|
||||
color="primary"
|
||||
class="cursor-pointer"
|
||||
rounded
|
||||
>
|
||||
<strong>
|
||||
<q-icon name="add" size="xs"></q-icon>
|
||||
<span v-text="$t('create_new_wallet')"></span>
|
||||
</strong>
|
||||
</q-badge>
|
||||
{%endif%}
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section
|
||||
v-if="authAction === 'login' && authMethod === 'user-id-only'"
|
||||
>
|
||||
<b> <span v-text="$t('login_with_user_id')"></span> </b><br /><br />
|
||||
<q-form @submit="loginUsr" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="usr"
|
||||
label="usr"
|
||||
type="password"
|
||||
></q-input>
|
||||
<div>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="usr == ''"
|
||||
type="submit"
|
||||
label="Login"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
{%endif%} {% if "username-password" in LNBITS_AUTH_METHODS %}
|
||||
<q-card-section
|
||||
v-if="authAction === 'login' && authMethod === 'username-password'"
|
||||
>
|
||||
<div class="q-mb-lg">
|
||||
<strong>
|
||||
<span v-text="$t('login_to_account')"></span>
|
||||
</strong>
|
||||
</div>
|
||||
<q-form @submit="login" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="username"
|
||||
name="username"
|
||||
:label="$t('username_or_email') + ' *'"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="password"
|
||||
name="password"
|
||||
:label="$t('password') + ' *'"
|
||||
type="password"
|
||||
></q-input>
|
||||
<div>
|
||||
<q-btn
|
||||
:disable="!username || !password"
|
||||
color="primary"
|
||||
type="submit"
|
||||
:label="$t('login')"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
<q-card-section
|
||||
v-if="authAction === 'register' && authMethod === 'username-password'"
|
||||
>
|
||||
<b> <span v-text="$t('create_account')"></span> </b><br /><br />
|
||||
<q-form @submit="register" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
required
|
||||
v-model="username"
|
||||
:label="$t('username') + ' *'"
|
||||
:rules="[(val) => validateUsername(val) || $t('invalid_username')]"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="password"
|
||||
:label="$t('password') + ' *'"
|
||||
type="password"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="passwordRepeat"
|
||||
:label="$t('password_repeat') + ' *'"
|
||||
type="password"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
></q-input>
|
||||
<div>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="!password || !passwordRepeat|| !username || (password !== passwordRepeat)"
|
||||
type="submit"
|
||||
class="full-width"
|
||||
:label="$t('create_account')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
{%endif%} {% if LNBITS_NEW_ACCOUNTS_ALLOWED %}
|
||||
<q-card-section
|
||||
v-if="authAction === 'register' && authMethod === 'user-id-only'"
|
||||
>
|
||||
<div>
|
||||
<q-form @submit="createWallet" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="walletName"
|
||||
:label='$t("name_your_wallet", { name: "{{ SITE_TITLE }} *" })'
|
||||
></q-input>
|
||||
<div>
|
||||
<q-btn
|
||||
color="primary"
|
||||
:disable="walletName == ''"
|
||||
type="submit"
|
||||
:label="$t('add_wallet')"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</q-card-section>
|
||||
{% if "username-password" in LNBITS_AUTH_METHODS %}
|
||||
<q-card-section
|
||||
v-if="authAction === 'login' && authMethod === 'username-password'"
|
||||
>
|
||||
<div>
|
||||
<q-btn
|
||||
color="grey"
|
||||
outline
|
||||
:label="$t('register')"
|
||||
class="full-width"
|
||||
@click="showRegister('username-password')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
{%endif%}
|
||||
<q-separator></q-separator>
|
||||
{% endif %}
|
||||
|
||||
<q-card-section
|
||||
v-if="authAction === 'login' && authMethod === 'username-password'"
|
||||
>
|
||||
<div class="row">
|
||||
{% if "google-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col-12 full-width q-pa-sm">
|
||||
<q-btn
|
||||
href="/api/v1/auth/google"
|
||||
type="a"
|
||||
outline
|
||||
no-caps
|
||||
rounded
|
||||
color="grey"
|
||||
class="full-width"
|
||||
>
|
||||
<q-avatar size="32px" class="q-mr-md">
|
||||
<q-img
|
||||
:src="'{{ static_url_for('static', 'images/google-logo.png') }}'"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
<div>
|
||||
<span v-text="$t('signin_with_google')"></span>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
|
||||
<div class="col-12 full-width q-pa-sm">
|
||||
<q-btn
|
||||
href="/api/v1/auth/github"
|
||||
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/github-logo.png') }}'"
|
||||
></q-img>
|
||||
</q-avatar>
|
||||
<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>
|
||||
<q-card-section v-else>
|
||||
<div class="row">
|
||||
<div class="col q-pa-sm">
|
||||
<q-btn
|
||||
@click="showLogin('username-password')"
|
||||
:label="$t('back')"
|
||||
outline
|
||||
rounded
|
||||
color="grey"
|
||||
class="full-width"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ads -->
|
||||
<div class="col-12 col-md-3 col-lg-3" v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
<div
|
||||
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
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://github.com/lnbits/lnbits"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:label="$t('view_github')"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:label="$t('donate')"
|
||||
class="full-width q-mb-lg"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn
|
||||
flat
|
||||
@@ -82,18 +347,26 @@
|
||||
></q-btn>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://github.com/ElementsProject/lightning">
|
||||
<a
|
||||
href="https://github.com/ElementsProject/lightning"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/cln.png' : '/static/images/clnl.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/cln.png') }}' : '{{ static_url_for('static', 'images/clnl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<a href="https://github.com/lightningnetwork/lnd">
|
||||
<a
|
||||
href="https://github.com/lightningnetwork/lnd"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/lnd.png' : '/static/images/lnd.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnd.png') }}' : '{{ static_url_for('static', 'images/lnd.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
@@ -101,18 +374,26 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://opennode.com">
|
||||
<a
|
||||
href="https://opennode.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/opennode.png' : '/static/images/opennodel.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/opennode.png') }}' : '{{ static_url_for('static', 'images/opennodel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<a href="https://lnpay.co/">
|
||||
<a
|
||||
href="https://lnpay.co/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/lnpay.png' : '/static/images/lnpayl.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnpay.png') }}' : '{{ static_url_for('static', 'images/lnpayl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
@@ -120,77 +401,125 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://github.com/rootzoll/raspiblitz">
|
||||
<a
|
||||
href="https://github.com/rootzoll/raspiblitz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/blitz.png' : '/static/images/blitzl.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blitz.png') }}' : '{{ static_url_for('static', 'images/blitzl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<a href="https://start9.com/">
|
||||
<a
|
||||
href="https://start9.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/start9.png' : '/static/images/start9l.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/start9.png') }}' : '{{ static_url_for('static', 'images/start9l.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://getumbrel.com/">
|
||||
<a
|
||||
href="https://getumbrel.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/umbrel.png' : '/static/images/umbrell.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/umbrel.png') }}' : '{{ static_url_for('static', 'images/umbrell.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<a href="https://mynodebtc.com">
|
||||
<a
|
||||
href="https://mynodebtc.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/mynode.png' : '/static/images/mynodel.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/mynode.png') }}' : '{{ static_url_for('static', 'images/mynodel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://github.com/shesek/spark-wallet">
|
||||
<a
|
||||
href="https://github.com/shesek/spark-wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/spark.png' : '/static/images/sparkl.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/spark.png') }}' : '{{ static_url_for('static', 'images/sparkl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<a href="https://voltage.cloud">
|
||||
<a
|
||||
href="https://voltage.cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/voltage.png' : '/static/images/voltagel.png'"
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/voltage.png') }}' : '{{ static_url_for('static', 'images/voltagel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a
|
||||
href="https://getalby.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/alby.png') }}' : '{{ static_url_for('static', 'images/albyl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<a href="https://zbd.gg" target="_blank" rel="noopener noreferrer">
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/zbd.png') }}' : '{{ static_url_for('static', 'images/zbdl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</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_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 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>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user, wallet) }}
|
||||
<script src="/core/static/js/wallet.js"></script>
|
||||
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
|
||||
{% endblock %}
|
||||
<!---->
|
||||
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
|
||||
@@ -15,46 +15,58 @@
|
||||
{% elif HIDE_API %}
|
||||
<div class="col-12 q-gutter-y-md">
|
||||
{% else %}
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<div
|
||||
class="col-12 col-md-7 q-gutter-y-md"
|
||||
:style="$q.screen.lt.md ? {
|
||||
position: mobileSimple ? 'fixed !important': ''
|
||||
, top: mobileSimple ? '50% !important': ''
|
||||
, left: mobileSimple ? '50% !important': ''
|
||||
, transform: mobileSimple ? 'translate(-50%, -50%) !important': ''
|
||||
} : ''"
|
||||
>
|
||||
{% endif %}
|
||||
<q-card>
|
||||
<q-card
|
||||
:style="$q.screen.lt.md ? {
|
||||
background: $q.screen.lt.md ? 'none !important': ''
|
||||
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
||||
, margin: $q.screen.lt.md && mobileSimple ? 'auto !important': ''
|
||||
, width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
|
||||
} : ''"
|
||||
>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none">
|
||||
<strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
|
||||
{{LNBITS_DENOMINATION}}
|
||||
<q-btn
|
||||
<h3 class="q-my-none text-no-wrap">
|
||||
<strong v-text="formattedBalance"></strong>
|
||||
<small>{{LNBITS_DENOMINATION}}</small>
|
||||
<lnbits-update-balance
|
||||
v-if="'{{user.super_user}}' == 'True'"
|
||||
:wallet_id="this.g.wallet.id"
|
||||
flat
|
||||
:callback="updateBalanceCallback"
|
||||
round
|
||||
color="primary"
|
||||
icon="add"
|
||||
size="md"
|
||||
>
|
||||
<q-popup-edit
|
||||
class="bg-accent text-white"
|
||||
v-slot="scope"
|
||||
v-model="credit"
|
||||
>
|
||||
<q-input
|
||||
filled
|
||||
:label='$t("credit_label", { denomination: "{{LNBITS_DENOMINATION}}"})'
|
||||
:hint="$t('credit_hint')"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
autofocus
|
||||
:mask="'{{LNBITS_DENOMINATION}}' != 'sats' ? '#.##' : '#'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="'{{LNBITS_DENOMINATION}}' != 'sats' ? '0.01' : '1'"
|
||||
@keyup.enter="updateBalance(scope.value)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="edit" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-popup-edit>
|
||||
</q-btn>
|
||||
/>
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div v-if="g.wallet.currency">
|
||||
<span
|
||||
class="text-h5 text-italic"
|
||||
v-text="formattedFiatBalance"
|
||||
style="opacity: 0.75"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
@click="mobileSimple = !mobileSimple"
|
||||
color="primary"
|
||||
class="float-right lt-md"
|
||||
size="sm"
|
||||
flat
|
||||
:icon="mobileSimple ? 'unfold_more' : 'unfold_less'"
|
||||
:label="mobileSimple ? $t('more') : $t('less')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<div class="row q-pb-md q-px-md q-col-gutter-md gt-sm">
|
||||
<div class="col">
|
||||
@@ -84,14 +96,20 @@
|
||||
:label="$t('scan')"
|
||||
>
|
||||
<q-tooltip
|
||||
>{% raw %}{{$t('camera_tooltip')}}{% endraw %}</q-tooltip
|
||||
>
|
||||
><span v-text="$t('camera_tooltip')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card
|
||||
:style="$q.screen.lt.md ? {
|
||||
background: $q.screen.lt.md ? 'none !important': ''
|
||||
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
||||
, marginTop: $q.screen.lt.md ? '0px !important': ''
|
||||
} : ''"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-sm">
|
||||
<div class="col">
|
||||
@@ -100,16 +118,13 @@
|
||||
:v-text="$t('transactions')"
|
||||
></h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="gt-sm col-auto">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey"
|
||||
@click="exportCSV"
|
||||
:label="$t('export_csv')"
|
||||
></q-btn>
|
||||
<!--<q-btn v-if="pendingPaymentsExist" dense flat round icon="update" color="grey" @click="checkPendingPayments">
|
||||
<q-tooltip>Check pending</q-tooltip>
|
||||
</q-btn>-->
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
@@ -118,17 +133,20 @@
|
||||
color="grey"
|
||||
@click="showChart"
|
||||
>
|
||||
<q-tooltip
|
||||
>{% raw %}{{$t('chart_tooltip')}}{% endraw %}</q-tooltip
|
||||
>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('chart_tooltip')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
:style="$q.screen.lt.md ? {
|
||||
display: mobileSimple ? 'none !important': ''
|
||||
} : ''"
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="paymentsTable.filter"
|
||||
v-model="paymentsTable.search"
|
||||
debounce="300"
|
||||
:placeholder="$t('search_by_tag_memo_amount')"
|
||||
class="q-mb-md"
|
||||
@@ -137,22 +155,26 @@
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="payments"
|
||||
:data="paymentsOmitter"
|
||||
:row-key="paymentTableRowKey"
|
||||
:columns="paymentsTable.columns"
|
||||
:pagination.sync="paymentsTable.pagination"
|
||||
:no-data-label="$t('no_transactions')"
|
||||
:filter="paymentsTable.filter"
|
||||
:filter="paymentsTable.search"
|
||||
:loading="paymentsTable.loading"
|
||||
:hide-header="mobileSimple"
|
||||
:hide-bottom="mobileSimple"
|
||||
@request="fetchPayments"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props"
|
||||
>{{ col.label }}</q-th
|
||||
>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
@@ -171,7 +193,9 @@
|
||||
color="grey"
|
||||
@click="props.expand = !props.expand"
|
||||
>
|
||||
<q-tooltip>{{$t('pending')}}</q-tooltip>
|
||||
<q-tooltip
|
||||
><span v-text="$t('pending')"></span
|
||||
></q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td
|
||||
@@ -185,47 +209,47 @@
|
||||
text-color="black"
|
||||
>
|
||||
<a
|
||||
v-text="'#'+props.row.tag"
|
||||
class="inherit"
|
||||
:href="['/', props.row.tag, '/?usr=', user.id].join('')"
|
||||
>
|
||||
#{{ props.row.tag }}
|
||||
</a>
|
||||
:href="['/', props.row.tag].join('')"
|
||||
></a>
|
||||
</q-badge>
|
||||
{{ props.row.memo }}
|
||||
<span v-text="props.row.memo"></span>
|
||||
<br />
|
||||
|
||||
<i>
|
||||
{{ props.row.dateFrom }}<q-tooltip
|
||||
>{{ props.row.date }}</q-tooltip
|
||||
></i
|
||||
>
|
||||
<span v-text="props.row.dateFrom"></span>
|
||||
<q-tooltip
|
||||
><span v-text="props.row.date"></span
|
||||
></q-tooltip>
|
||||
</i>
|
||||
</q-td>
|
||||
|
||||
{% endraw %}
|
||||
<q-td
|
||||
auto-width
|
||||
key="amount"
|
||||
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
||||
:props="props"
|
||||
>{% raw %} {{
|
||||
parseFloat(String(props.row.fsat).replaceAll(",", "")) / 100
|
||||
}}
|
||||
v-text="parseFloat(String(props.row.fsat).replaceAll(',', '')) / 100"
|
||||
>
|
||||
</q-td>
|
||||
<q-td auto-width key="amount" v-else :props="props">
|
||||
{{ props.row.fsat }}<br />
|
||||
<span v-text="props.row.fsat"></span>
|
||||
<br />
|
||||
<i v-if="props.row.extra.wallet_fiat_currency">
|
||||
{{ props.row.extra.wallet_fiat_currency }} {{
|
||||
props.row.extra.wallet_fiat_amount }}
|
||||
<span
|
||||
v-text="formatFiat(props.row.extra.wallet_fiat_currency, props.row.extra.wallet_fiat_amount)"
|
||||
></span>
|
||||
<br />
|
||||
</i>
|
||||
<i v-if="props.row.extra.fiat_currency">
|
||||
{{ props.row.extra.fiat_currency }} {{
|
||||
props.row.extra.fiat_amount }}
|
||||
<span
|
||||
v-text="formatFiat(props.row.extra.fiat_currency, props.row.extra.fiat_amount)"
|
||||
></span>
|
||||
</i>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<q-dialog v-model="props.expand" :props="props">
|
||||
<q-dialog v-model="props.expand" :props="props" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<div v-if="props.row.isIn && props.row.pending">
|
||||
@@ -295,30 +319,28 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% if HIDE_API %}
|
||||
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||
{% else %}
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<div
|
||||
v-if="!mobileSimple || $q.screen.gt.sm"
|
||||
class="col-12 col-md-5 q-gutter-y-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||
{{ SITE_TITLE }} Wallet:
|
||||
<strong><em>{{ wallet.name }}</em></strong>
|
||||
<strong><em>{{wallet.name}}</em></strong>
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-list>
|
||||
{% include "core/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
|
||||
{% if wallet.lnurlwithdraw_full %}
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
@@ -348,7 +370,8 @@
|
||||
<q-card-section class="text-center">
|
||||
<p v-text="$t('export_to_phone_desc')"></p>
|
||||
<qrcode
|
||||
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
|
||||
:value="'{{request.base_url}}wallet?usr={{user.id}}&wal={{wallet.id}}'"
|
||||
:options="{width:240}"
|
||||
></qrcode>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -430,6 +453,9 @@
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-separator></q-separator>
|
||||
{% include "core/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -462,17 +488,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show">
|
||||
{% raw %}
|
||||
<q-dialog v-model="receive.show" position="top">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<q-form @submit="createInvoice" class="q-gutter-md">
|
||||
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
||||
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
|
||||
<b v-text="receive.lnurl.domain"></b> is requesting an invoice:
|
||||
</p>
|
||||
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
|
||||
{% if LNBITS_DENOMINATION != 'sats' %}
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
@@ -516,7 +541,6 @@
|
||||
v-model.trim="receive.data.memo"
|
||||
:label="$t('memo')"
|
||||
></q-input>
|
||||
{% raw %}
|
||||
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
@@ -524,9 +548,10 @@
|
||||
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="receive.lnurl">
|
||||
{{$t('withdraw_from')}} {{receive.lnurl.domain}}
|
||||
</span>
|
||||
<span
|
||||
v-if="receive.lnurl"
|
||||
v-text="$t('withdraw_from') + receive.lnurl.domain"
|
||||
></span>
|
||||
<span v-else v-text="$t('create_invoice')"></span>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@@ -544,7 +569,10 @@
|
||||
></q-spinner>
|
||||
</q-form>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-card
|
||||
v-else-if="receive.paymentReq && receive.lnurl == null"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
@@ -570,28 +598,32 @@
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
{% endraw %}
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="parse.show" @hide="closeParseDialog">
|
||||
<q-dialog v-model="parse.show" @hide="closeParseDialog" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div v-if="parse.invoice">
|
||||
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
|
||||
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",",
|
||||
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
||||
</h6>
|
||||
<h6 v-else class="q-my-none">
|
||||
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {%
|
||||
raw %}
|
||||
</h6>
|
||||
<h6
|
||||
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
||||
class="q-my-none"
|
||||
v-text="parseFloat(String(parse.invoice.fsat).replaceAll(',', '')) / 100 + '{{LNBITS_DENOMINATION}}'"
|
||||
></h6>
|
||||
<h6
|
||||
v-else
|
||||
class="q-my-none"
|
||||
v-text="parse.invoice.fsat + '{{LNBITS_DENOMINATION}}'"
|
||||
></h6>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<p class="text-wrap">
|
||||
<strong v-text="$t('memo')">:</strong> {{
|
||||
parse.invoice.description }}<br />
|
||||
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
|
||||
<strong>Hash:</strong> {{ parse.invoice.hash }}
|
||||
<strong v-text="$t('memo') + ': '"></strong>
|
||||
<span v-text="parse.invoice.description"></span>
|
||||
<br />
|
||||
<strong>Expire date: </strong>
|
||||
<span v-text="parse.invoice.expireDate"></span>
|
||||
<br />
|
||||
<strong>Hash: </strong>
|
||||
<span v-text="parse.invoice.hash"></span>
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div v-if="canPay" class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
@@ -625,24 +657,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="parse.lnurlauth">
|
||||
{% raw %}
|
||||
<q-form @submit="authLnurl" class="q-gutter-md">
|
||||
<p class="q-my-none text-h6">
|
||||
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
|
||||
Authenticate with <b v-text="parse.lnurlauth.domain"></b>?
|
||||
</p>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<p>
|
||||
For every website and for every LNbits wallet, a new keypair
|
||||
will be deterministically generated so your identity can't be
|
||||
tied to your LNbits wallet or linked across websites. No other
|
||||
data will be shared with {{ parse.lnurlauth.domain }}.
|
||||
data will be shared with
|
||||
<span v-text="parse.lnurlauth.domain"></span>.
|
||||
</p>
|
||||
<p>
|
||||
Your public key for <b v-text="parse.lnurlauth.domain"></b> is:
|
||||
</p>
|
||||
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
|
||||
<p class="q-mx-xl">
|
||||
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
|
||||
<code class="text-wrap" v-text="parse.lnurlauth.pubkey"></code>
|
||||
</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" type="submit">Login</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
:label="$t('login')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
:label="$t('cancel')"
|
||||
v-close-popup
|
||||
@@ -652,55 +691,74 @@
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
{% endraw %}
|
||||
</div>
|
||||
<div v-else-if="parse.lnurlpay">
|
||||
{% raw %}
|
||||
<q-form @submit="payLnurl" class="q-gutter-md">
|
||||
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
||||
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
|
||||
parse.lnurlpay.maxSendable | msatoshiFormat }}
|
||||
{{LNBITS_DENOMINATION}}
|
||||
<b v-text="parse.lnurlpay.domain"></b> is requesting
|
||||
<span
|
||||
v-text="msatoshiFormat(parse.lnurlpay.maxSendable)"
|
||||
></span>
|
||||
<span v-text="'{{LNBITS_DENOMINATION}}'"></span>
|
||||
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||
<br />
|
||||
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
||||
and a
|
||||
<span v-text="parse.lnurlpay.commentAllowed"></span>-char
|
||||
comment
|
||||
</span>
|
||||
</p>
|
||||
<p v-else class="q-my-none text-h6 text-center">
|
||||
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b>
|
||||
<b
|
||||
v-text="parse.lnurlpay.targetUser || parse.lnurlpay.domain"
|
||||
></b>
|
||||
is requesting <br />
|
||||
between
|
||||
<b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
|
||||
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
|
||||
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
||||
<b v-text="msatoshiFormat(parse.lnurlpay.minSendable)"></b> and
|
||||
<b v-text="msatoshiFormat(parse.lnurlpay.maxSendable)"></b>
|
||||
<span v-text="'{{LNBITS_DENOMINATION}}'"></span>
|
||||
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||
<br />
|
||||
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
||||
and a
|
||||
<span v-text="parse.lnurlpay.commentAllowed"></span>-char
|
||||
comment
|
||||
</span>
|
||||
</p>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<div class="row">
|
||||
<p class="col text-justify text-italic">
|
||||
{{ parse.lnurlpay.description }}
|
||||
</p>
|
||||
<p
|
||||
class="col text-justify text-italic"
|
||||
v-text="parse.lnurlpay.description"
|
||||
></p>
|
||||
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
|
||||
<q-img :src="parse.lnurlpay.image" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% endraw %}
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-if="!parse.lnurlpay.fixed"
|
||||
v-model="parse.data.unit"
|
||||
type="text"
|
||||
:label="$t('unit')"
|
||||
:options="receive.units"
|
||||
></q-select>
|
||||
<br />
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.number="parse.data.amount"
|
||||
type="number"
|
||||
label="Amount ({{LNBITS_DENOMINATION}}) *"
|
||||
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
||||
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
|
||||
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:min="parse.lnurlpay.minSendable / 1000"
|
||||
:max="parse.lnurlpay.maxSendable / 1000"
|
||||
:readonly="parse.lnurlpay.fixed"
|
||||
:readonly="parse.lnurlpay && parse.lnurlpay.fixed"
|
||||
></q-input>
|
||||
{% raw %}
|
||||
</div>
|
||||
<div
|
||||
class="col-8 q-pl-md"
|
||||
@@ -717,9 +775,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" type="submit"
|
||||
>Send {{LNBITS_DENOMINATION}}</q-btn
|
||||
>
|
||||
<q-btn unelevated color="primary" type="submit">Send</q-btn>
|
||||
<q-btn
|
||||
:label="$t('cancel')"
|
||||
v-close-popup
|
||||
@@ -729,7 +785,6 @@
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
{% endraw %}
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-form
|
||||
@@ -760,11 +815,11 @@
|
||||
class="q-mt-xs q-ml-sm q-mr-auto"
|
||||
v-if="parse.copy.show"
|
||||
@click="pasteToTextArea"
|
||||
><q-tooltip
|
||||
>{% raw %}{{$t('paste_from_clipboard')}}{% endraw
|
||||
%}</q-tooltip
|
||||
></q-icon
|
||||
>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('paste_from_clipboard')"></span>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
@@ -797,7 +852,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="parse.camera.show">
|
||||
<q-dialog v-model="parse.camera.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl">
|
||||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream
|
||||
@@ -818,7 +873,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="paymentsChart.show">
|
||||
<q-dialog v-model="paymentsChart.show" position="top">
|
||||
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||
<q-card-section>
|
||||
<div class="row q-gutter-sm justify-between">
|
||||
@@ -865,16 +920,20 @@
|
||||
</q-btn>
|
||||
</q-tabs>
|
||||
|
||||
<q-dialog v-model="disclaimerDialog.show">
|
||||
<q-dialog v-model="disclaimerDialog.show" position="top">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">Warning</h6>
|
||||
<p v-text="$t('disclaimer_dialog')"></p>
|
||||
<h6
|
||||
class="q-my-md text-primary"
|
||||
v-text="$t('disclaimer_dialog_title')"
|
||||
></h6>
|
||||
<p class="whitespace-pre-line" v-text="$t('disclaimer_dialog')"></p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(disclaimerDialog.location.href)"
|
||||
:label="$t('copy_wallet_url')"
|
||||
type="a"
|
||||
href="/account"
|
||||
:label="$t('my_account')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
|
||||
@@ -150,7 +150,6 @@
|
||||
Open channel
|
||||
</q-btn>
|
||||
</div>
|
||||
{% raw %}
|
||||
<div>
|
||||
<div class="text-subtitle1 col-grow">Total</div>
|
||||
<lnbits-channel-balance
|
||||
@@ -172,11 +171,12 @@
|
||||
<q-tr :props="props">
|
||||
<div class="q-pb-sm">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<div class="text-subtitle1 col-grow">
|
||||
{{props.row.name}}
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle1 col-grow"
|
||||
v-text="props.row.name"
|
||||
></div>
|
||||
<div class="text-caption" v-if="props.row.short_id">
|
||||
{{ props.row.short_id }}
|
||||
<span v-text="props.row.short_id"></span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
@@ -188,9 +188,8 @@
|
||||
<q-badge
|
||||
rounded
|
||||
:color="states.find(s => s.value == props.row.state)?.color"
|
||||
v-text="states.find(s => s.value == props.row.state)?.label"
|
||||
>
|
||||
{{ states.find(s => s.value == props.row.state)?.label
|
||||
}}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
:disable='props.row.state !== "active"'
|
||||
@@ -210,15 +209,12 @@
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<q-card class="full-height">
|
||||
<q-card-section class="column q-gutter-y-sm">
|
||||
{% raw %}
|
||||
<div
|
||||
class="row items-center q-mt-none justify-between q-gutter-x-md no-wrap"
|
||||
>
|
||||
@@ -254,19 +250,21 @@
|
||||
<q-tr :props="props">
|
||||
<div class="row no-wrap items-center q-gutter-sm">
|
||||
<div class="q-my-sm col-grow">
|
||||
<div class="text-subtitle1 text-bold">
|
||||
{{ props.row.alias }}
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle1 text-bold"
|
||||
v-text="props.row.alias"
|
||||
></div>
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-badge
|
||||
:style="`background-color: #${props.row.color}`"
|
||||
class="text-bold"
|
||||
v-text="'#'+props.row.color"
|
||||
>
|
||||
#{{ props.row.color }}
|
||||
</q-badge>
|
||||
<div class="text-bold">
|
||||
{{ shortenNodeId(props.row.id) }}
|
||||
</div>
|
||||
<div
|
||||
class="text-bold"
|
||||
v-text="shortenNodeId(props.row.id)"
|
||||
></div>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
@@ -302,8 +300,6 @@
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<q-tab-panel name="dashboard">
|
||||
<q-card-section class="q-pa-none">
|
||||
{% raw %}
|
||||
<lnbits-node-info :info="this.info"></lnbits-node-info>
|
||||
<div class="row q-col-gutter-lg q-mt-sm">
|
||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||
<div class="row q-col-gutter-md q-pb-lg">
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
title="Total Capacity"
|
||||
:title="$t('total_capacity')"
|
||||
:msat="this.channel_stats.total_capacity"
|
||||
/>
|
||||
</div>
|
||||
@@ -65,6 +64,5 @@
|
||||
></lnbits-channel-stats>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<q-dialog v-model="transactionDetailsDialog.show">
|
||||
<q-card class="my-card">
|
||||
<q-card-section>
|
||||
{% raw %}
|
||||
<div class="text-center q-mb-lg">
|
||||
<div
|
||||
v-if="transactionDetailsDialog.data.isIn && transactionDetailsDialog.data.pending"
|
||||
@@ -18,7 +17,9 @@
|
||||
<div class="row q-my-md">
|
||||
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
{{ transactionDetailsDialog.data.payment_hash }}
|
||||
<span
|
||||
v-text="transactionDetailsDialog.data.payment_hash"
|
||||
></span>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(transactionDetailsDialog.data.payment_hash)"
|
||||
@@ -33,7 +34,7 @@
|
||||
>
|
||||
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
{{ transactionDetailsDialog.data.preimage }}
|
||||
<span v-text="transactionDetailsDialog.data.preimage"></span>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(transactionDetailsDialog.data.preimage)"
|
||||
@@ -66,7 +67,6 @@
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -102,7 +102,6 @@
|
||||
:filter="paymentsTable.filter"
|
||||
@request="getPayments"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:body-cell-pending="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
@@ -211,9 +210,8 @@
|
||||
<q-badge
|
||||
:style="`background-color: #${props.row.destination?.color}`"
|
||||
class="text-bold"
|
||||
>
|
||||
{{ props.row.destination?.alias }}
|
||||
</q-badge>
|
||||
v-text="props.row.destination?.alias"
|
||||
></q-badge>
|
||||
<div>
|
||||
<q-btn
|
||||
size="xs"
|
||||
@@ -233,7 +231,6 @@
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -266,7 +263,6 @@
|
||||
:filter="invoiceTable.filter"
|
||||
@request="getInvoices"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:body-cell-pending="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
@@ -305,8 +301,6 @@
|
||||
></lnbits-date>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="/core/static/js/node.js"></script>
|
||||
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
@@ -249,7 +249,6 @@
|
||||
},
|
||||
api: function (method, url, options) {
|
||||
const params = new URLSearchParams(options?.query)
|
||||
params.set('usr', this.g.user.id)
|
||||
return LNbits.api
|
||||
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
|
||||
.catch(error => {
|
||||
|
||||
@@ -9,7 +9,7 @@ context %} {% block page %}
|
||||
<div class="row q-col-gutter-md q-pb-lg">
|
||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
title="Total Capacity"
|
||||
:title="$t('total_capacity')"
|
||||
:msat="this.channel_stats.total_capacity"
|
||||
/>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@ context %} {% block page %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="/core/static/js/node.js"></script>
|
||||
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// update cache version every time there is a new deployment
|
||||
// so the service worker reinitializes the cache
|
||||
const CACHE_VERSION = 64
|
||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
const CURRENT_CACHE = 'lnbits-{{ cache_version }}-'
|
||||
|
||||
const getApiKey = request => {
|
||||
let api_key = request.headers.get('X-Api-Key')
|
||||
@@ -17,8 +16,7 @@ self.addEventListener('activate', evt =>
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
const currentCacheVersion = cacheName.split('-').slice(-2, 2)
|
||||
if (currentCacheVersion !== CACHE_VERSION) {
|
||||
if (!cacheName.startsWith(CURRENT_CACHE)) {
|
||||
return caches.delete(cacheName)
|
||||
}
|
||||
})
|
||||
@@ -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),
|
||||
|
||||
+134
-164
@@ -1,12 +1,12 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from math import ceil
|
||||
from typing import Dict, List, Optional, Union
|
||||
from urllib.parse import ParseResult, parse_qs, unquote, urlencode, urlparse, urlunparse
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import httpx
|
||||
import pyqrcode
|
||||
@@ -23,21 +23,21 @@ from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from loguru import logger
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
from starlette.responses import RedirectResponse, StreamingResponse
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from lnbits import bolt11, lnurl
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.db import core_app_extra, db
|
||||
from lnbits.core.helpers import (
|
||||
migrate_extension_database,
|
||||
stop_extension_background_work,
|
||||
)
|
||||
from lnbits.core.models import (
|
||||
BaseWallet,
|
||||
ConversionData,
|
||||
CreateInvoice,
|
||||
CreateLnurl,
|
||||
CreateLnurlAuth,
|
||||
CreateWallet,
|
||||
CreateWebPushSubscription,
|
||||
DecodePayment,
|
||||
Payment,
|
||||
PaymentFilters,
|
||||
@@ -46,12 +46,13 @@ from lnbits.core.models import (
|
||||
User,
|
||||
Wallet,
|
||||
WalletType,
|
||||
WebPushSubscription,
|
||||
)
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_access_token,
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
get_key_type,
|
||||
parse_filters,
|
||||
require_admin_key,
|
||||
@@ -63,12 +64,14 @@ from lnbits.extension_manager import (
|
||||
ExtensionRelease,
|
||||
InstallableExtension,
|
||||
fetch_github_release_config,
|
||||
fetch_release_payment_info,
|
||||
get_valid_extensions,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi, url_for
|
||||
from lnbits.lnurl import decode as lnurl_decode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
allowed_currencies,
|
||||
fiat_amount_as_satoshis,
|
||||
satoshis_amount_as_fiat,
|
||||
)
|
||||
@@ -77,24 +80,18 @@ from ..crud import (
|
||||
DateTrunc,
|
||||
add_installed_extension,
|
||||
create_account,
|
||||
create_tinyurl,
|
||||
create_wallet,
|
||||
create_webpush_subscription,
|
||||
delete_dbversion,
|
||||
delete_installed_extension,
|
||||
delete_tinyurl,
|
||||
delete_wallet,
|
||||
delete_webpush_subscription,
|
||||
drop_extension_db,
|
||||
get_dbversions,
|
||||
get_installed_extension,
|
||||
get_payments,
|
||||
get_payments_history,
|
||||
get_payments_paginated,
|
||||
get_standalone_payment,
|
||||
get_tinyurl,
|
||||
get_tinyurl_by_url,
|
||||
get_wallet_for_key,
|
||||
get_webpush_subscription,
|
||||
save_balance_check,
|
||||
update_pending_payments,
|
||||
update_wallet,
|
||||
@@ -104,6 +101,7 @@ from ..services import (
|
||||
PaymentFailure,
|
||||
check_transaction_status,
|
||||
create_invoice,
|
||||
fee_reserve_total,
|
||||
pay_invoice,
|
||||
perform_lnurlauth,
|
||||
websocketManager,
|
||||
@@ -131,6 +129,15 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/wallets",
|
||||
name="Wallets",
|
||||
description="Get basic info for all of user's wallets.",
|
||||
)
|
||||
async def api_wallets(user: User = Depends(check_user_exists)) -> List[BaseWallet]:
|
||||
return [BaseWallet(**w.dict()) for w in user.wallets]
|
||||
|
||||
|
||||
@api_router.put("/api/v1/wallet/{new_name}")
|
||||
async def api_update_wallet_name(
|
||||
new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
@@ -296,7 +303,8 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
||||
if data.lnurl_balance_check is not None:
|
||||
await save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
try:
|
||||
r = await client.get(
|
||||
data.lnurl_callback,
|
||||
@@ -331,9 +339,13 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
||||
}
|
||||
|
||||
|
||||
async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||
async def api_payments_pay_invoice(
|
||||
bolt11: str, wallet: Wallet, extra: Optional[dict] = None
|
||||
):
|
||||
try:
|
||||
payment_hash = await pay_invoice(wallet_id=wallet.id, payment_request=bolt11)
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=wallet.id, payment_request=bolt11, extra=extra
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||
except PermissionError as e:
|
||||
@@ -374,7 +386,7 @@ async def api_payments_create(
|
||||
detail="BOLT11 string is invalid or not given",
|
||||
)
|
||||
return await api_payments_pay_invoice(
|
||||
invoiceData.bolt11, wallet.wallet
|
||||
invoiceData.bolt11, wallet.wallet, invoiceData.extra
|
||||
) # admin key
|
||||
elif not invoiceData.out:
|
||||
# invoice key
|
||||
@@ -386,21 +398,44 @@ async def api_payments_create(
|
||||
)
|
||||
|
||||
|
||||
@api_router.get("/api/v1/payments/fee-reserve")
|
||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
if invoice_obj.amount_msat:
|
||||
response = {
|
||||
"fee_reserve": fee_reserve_total(invoice_obj.amount_msat),
|
||||
}
|
||||
return JSONResponse(response)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Invoice has no amount.",
|
||||
)
|
||||
|
||||
|
||||
@api_router.post("/api/v1/payments/lnurl")
|
||||
async def api_payments_pay_lnurl(
|
||||
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
domain = urlparse(data.callback).netloc
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||
try:
|
||||
if data.unit and data.unit != "sat":
|
||||
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
|
||||
# no msat precision
|
||||
amount_msat = ceil(amount_msat // 1000) * 1000
|
||||
else:
|
||||
amount_msat = data.amount
|
||||
r = await client.get(
|
||||
data.callback,
|
||||
params={"amount": data.amount, "comment": data.comment},
|
||||
params={"amount": amount_msat, "comment": data.comment},
|
||||
timeout=40,
|
||||
)
|
||||
if r.is_error:
|
||||
raise httpx.ConnectError("LNURL callback connection error")
|
||||
r.raise_for_status()
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
@@ -421,13 +456,13 @@ async def api_payments_pay_lnurl(
|
||||
)
|
||||
|
||||
invoice = bolt11.decode(params["pr"])
|
||||
if invoice.amount_msat != data.amount:
|
||||
if invoice.amount_msat != amount_msat:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=(
|
||||
(
|
||||
f"{domain} returned an invalid invoice. Expected"
|
||||
f" {data.amount} msat, got {invoice.amount_msat}."
|
||||
f" {amount_msat} msat, got {invoice.amount_msat}."
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -438,6 +473,9 @@ async def api_payments_pay_lnurl(
|
||||
extra["success_action"] = params["successAction"]
|
||||
if data.comment:
|
||||
extra["comment"] = data.comment
|
||||
if data.unit and data.unit != "sat":
|
||||
extra["fiat_currency"] = data.unit
|
||||
extra["fiat_amount"] = data.amount / 1000
|
||||
assert data.description is not None, "description is required"
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=wallet.wallet.id,
|
||||
@@ -501,10 +539,7 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||
# We use X_Api_Key here because we want this call to work with and without keys
|
||||
# If a valid key is given, we also return the field "details", otherwise not
|
||||
wallet = await get_wallet_for_key(X_Api_Key) if isinstance(X_Api_Key, str) else None
|
||||
wallet = wallet if wallet and not wallet.deleted else None
|
||||
# we have to specify the wallet id here, because postgres and sqlite return
|
||||
# internal payments in different order and get_standalone_payment otherwise
|
||||
# just fetches the first one, causing unpredictable results
|
||||
|
||||
payment = await get_standalone_payment(
|
||||
payment_hash, wallet_id=wallet.id if wallet else None
|
||||
)
|
||||
@@ -544,7 +579,7 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||
@api_router.get("/api/v1/lnurlscan/{code}")
|
||||
async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
try:
|
||||
url = lnurl.decode(code)
|
||||
url = str(lnurl_decode(code))
|
||||
domain = urlparse(url).netloc
|
||||
except Exception:
|
||||
# parse internet identifier (user@domain.com)
|
||||
@@ -574,7 +609,8 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
||||
assert lnurlauth_key.verifying_key
|
||||
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||
else:
|
||||
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||
r = await client.get(url, timeout=5)
|
||||
r.raise_for_status()
|
||||
if r.is_error:
|
||||
@@ -661,7 +697,7 @@ async def api_payments_decode(data: DecodePayment) -> JSONResponse:
|
||||
payment_str = data.data
|
||||
try:
|
||||
if payment_str[:5] == "LNURL":
|
||||
url = lnurl.decode(payment_str)
|
||||
url = str(lnurl_decode(payment_str))
|
||||
return JSONResponse({"domain": url})
|
||||
else:
|
||||
invoice = bolt11.decode(payment_str)
|
||||
@@ -686,14 +722,8 @@ async def api_perform_lnurlauth(
|
||||
|
||||
|
||||
@api_router.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
if len(settings.lnbits_allowed_currencies) > 0:
|
||||
return [
|
||||
item
|
||||
for item in currencies.keys()
|
||||
if item.upper() in settings.lnbits_allowed_currencies
|
||||
]
|
||||
return list(currencies.keys())
|
||||
async def api_list_currencies_available() -> List[str]:
|
||||
return allowed_currencies()
|
||||
|
||||
|
||||
@api_router.post("/api/v1/conversion")
|
||||
@@ -765,10 +795,12 @@ async def websocket_update_get(item_id: str, data: str):
|
||||
|
||||
@api_router.post("/api/v1/extension")
|
||||
async def api_install_extension(
|
||||
data: CreateExtension, user: User = Depends(check_admin)
|
||||
data: CreateExtension,
|
||||
user: User = Depends(check_admin),
|
||||
access_token: Optional[str] = Depends(check_access_token),
|
||||
):
|
||||
release = await InstallableExtension.get_extension_release(
|
||||
data.ext_id, data.source_repo, data.archive
|
||||
data.ext_id, data.source_repo, data.archive, data.version
|
||||
)
|
||||
if not release:
|
||||
raise HTTPException(
|
||||
@@ -780,13 +812,17 @@ async def api_install_extension(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
|
||||
)
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
ext_info = InstallableExtension(
|
||||
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
|
||||
)
|
||||
|
||||
ext_info.download_archive()
|
||||
|
||||
try:
|
||||
installed_ext = await get_installed_extension(data.ext_id)
|
||||
ext_info.payments = installed_ext.payments if installed_ext else []
|
||||
|
||||
await ext_info.download_archive()
|
||||
|
||||
ext_info.extract_archive()
|
||||
|
||||
extension = Extension.from_installable_ext(ext_info)
|
||||
@@ -796,8 +832,9 @@ async def api_install_extension(
|
||||
|
||||
await add_installed_extension(ext_info)
|
||||
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(data.ext_id, user.id)
|
||||
if extension.is_upgrade_extension:
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(data.ext_id, user.id, access_token)
|
||||
|
||||
if data.ext_id not in settings.lnbits_deactivated_extensions:
|
||||
settings.lnbits_deactivated_extensions += [data.ext_id]
|
||||
@@ -809,7 +846,8 @@ async def api_install_extension(
|
||||
ext_info.nofiy_upgrade()
|
||||
|
||||
return extension
|
||||
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
ext_info.clean_extension_files()
|
||||
@@ -823,7 +861,11 @@ async def api_install_extension(
|
||||
|
||||
|
||||
@api_router.delete("/api/v1/extension/{ext_id}")
|
||||
async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)):
|
||||
async def api_uninstall_extension(
|
||||
ext_id: str,
|
||||
user: User = Depends(check_admin),
|
||||
access_token: Optional[str] = Depends(check_access_token),
|
||||
):
|
||||
installable_extensions = await InstallableExtension.get_installable_extensions()
|
||||
|
||||
extensions = [e for e in installable_extensions if e.id == ext_id]
|
||||
@@ -849,7 +891,7 @@ async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)
|
||||
|
||||
try:
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(ext_id, user.id)
|
||||
await stop_extension_background_work(ext_id, user.id, access_token)
|
||||
|
||||
if ext_id not in settings.lnbits_deactivated_extensions:
|
||||
settings.lnbits_deactivated_extensions += [ext_id]
|
||||
@@ -870,9 +912,18 @@ async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)
|
||||
)
|
||||
async def get_extension_releases(ext_id: str):
|
||||
try:
|
||||
extension_releases: List[
|
||||
ExtensionRelease
|
||||
] = await InstallableExtension.get_extension_releases(ext_id)
|
||||
extension_releases: List[ExtensionRelease] = (
|
||||
await InstallableExtension.get_extension_releases(ext_id)
|
||||
)
|
||||
|
||||
installed_ext = await get_installed_extension(ext_id)
|
||||
if not installed_ext:
|
||||
return extension_releases
|
||||
|
||||
for release in extension_releases:
|
||||
payment_info = installed_ext.find_existing_payment(release.pay_link)
|
||||
if payment_info:
|
||||
release.paid_sats = payment_info.amount
|
||||
|
||||
return extension_releases
|
||||
|
||||
@@ -882,6 +933,40 @@ async def get_extension_releases(ext_id: str):
|
||||
)
|
||||
|
||||
|
||||
@api_router.put("/api/v1/extension/invoice", dependencies=[Depends(check_admin)])
|
||||
async def get_extension_invoice(data: CreateExtension):
|
||||
try:
|
||||
assert data.cost_sats, "A non-zero amount must be specified"
|
||||
release = await InstallableExtension.get_extension_release(
|
||||
data.ext_id, data.source_repo, data.archive, data.version
|
||||
)
|
||||
assert release, "Release not found"
|
||||
assert release.pay_link, "Pay link not found for release"
|
||||
|
||||
payment_info = await fetch_release_payment_info(
|
||||
release.pay_link, data.cost_sats
|
||||
)
|
||||
assert payment_info and payment_info.payment_request, "Cannot request invoice"
|
||||
invoice = bolt11.decode(payment_info.payment_request)
|
||||
|
||||
assert invoice.amount_msat is not None, "Invoic amount is missing"
|
||||
invoice_amount = int(invoice.amount_msat / 1000)
|
||||
assert (
|
||||
invoice_amount == data.cost_sats
|
||||
), f"Wrong invoice amount: {invoice_amount}."
|
||||
assert (
|
||||
payment_info.payment_hash == invoice.payment_hash
|
||||
), "Wroong invoice payment hash"
|
||||
|
||||
return payment_info
|
||||
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice")
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/extension/release/{org}/{repo}/{tag_name}",
|
||||
dependencies=[Depends(check_admin)],
|
||||
@@ -927,118 +1012,3 @@ async def delete_extension_db(ext_id: str):
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Cannot delete data for extension '{ext_id}'",
|
||||
)
|
||||
|
||||
|
||||
@api_router.post(
|
||||
"/api/v1/tinyurl",
|
||||
name="Tinyurl",
|
||||
description="creates a tinyurl",
|
||||
)
|
||||
async def api_create_tinyurl(
|
||||
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
tinyurls = await get_tinyurl_by_url(url)
|
||||
try:
|
||||
for tinyurl in tinyurls:
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
return tinyurl
|
||||
return await create_tinyurl(url, endless, wallet.wallet.inkey)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to create tinyurl"
|
||||
)
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/tinyurl/{tinyurl_id}",
|
||||
name="Tinyurl",
|
||||
description="get a tinyurl by id",
|
||||
)
|
||||
async def api_get_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
return tinyurl
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Unable to fetch tinyurl"
|
||||
)
|
||||
|
||||
|
||||
@api_router.delete(
|
||||
"/api/v1/tinyurl/{tinyurl_id}",
|
||||
name="Tinyurl",
|
||||
description="delete a tinyurl by id",
|
||||
)
|
||||
async def api_delete_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
await delete_tinyurl(tinyurl_id)
|
||||
return {"deleted": True}
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to delete"
|
||||
)
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/t/{tinyurl_id}",
|
||||
name="Tinyurl",
|
||||
description="redirects a tinyurl by id",
|
||||
)
|
||||
async def api_tinyurl(tinyurl_id: str):
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
response = RedirectResponse(url=tinyurl.url)
|
||||
return response
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="unable to find tinyurl"
|
||||
)
|
||||
|
||||
|
||||
@api_router.post("/api/v1/webpush", status_code=HTTPStatus.CREATED)
|
||||
async def api_create_webpush_subscription(
|
||||
request: Request,
|
||||
data: CreateWebPushSubscription,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> WebPushSubscription:
|
||||
subscription = json.loads(data.subscription)
|
||||
endpoint = subscription["endpoint"]
|
||||
host = urlparse(str(request.url)).netloc
|
||||
|
||||
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
if subscription:
|
||||
return subscription
|
||||
else:
|
||||
return await create_webpush_subscription(
|
||||
endpoint,
|
||||
wallet.wallet.user,
|
||||
data.subscription,
|
||||
host,
|
||||
)
|
||||
|
||||
|
||||
@api_router.delete("/api/v1/webpush", status_code=HTTPStatus.OK)
|
||||
async def api_delete_webpush_subscription(
|
||||
request: Request,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
endpoint = unquote(
|
||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||
)
|
||||
await delete_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
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, SSOBase
|
||||
from loguru import logger
|
||||
from starlette.status import (
|
||||
HTTP_400_BAD_REQUEST,
|
||||
HTTP_401_UNAUTHORIZED,
|
||||
HTTP_403_FORBIDDEN,
|
||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import (
|
||||
create_access_token,
|
||||
decrypt_internal_message,
|
||||
encrypt_internal_message,
|
||||
is_valid_email_address,
|
||||
is_valid_username,
|
||||
)
|
||||
from lnbits.settings import AuthMethods, settings
|
||||
|
||||
from ..crud import (
|
||||
create_account,
|
||||
create_user,
|
||||
get_account,
|
||||
get_account_by_email,
|
||||
get_account_by_username_or_email,
|
||||
get_user,
|
||||
update_account,
|
||||
update_user_password,
|
||||
verify_user_password,
|
||||
)
|
||||
from ..models import (
|
||||
CreateUser,
|
||||
LoginUsernamePassword,
|
||||
LoginUsr,
|
||||
UpdateSuperuserPassword,
|
||||
UpdateUser,
|
||||
UpdateUserPassword,
|
||||
User,
|
||||
UserConfig,
|
||||
)
|
||||
|
||||
auth_router = APIRouter()
|
||||
|
||||
|
||||
@auth_router.get("/api/v1/auth", description="Get the authenticated user")
|
||||
async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
|
||||
return user
|
||||
|
||||
|
||||
@auth_router.post("/api/v1/auth", description="Login via the username and password")
|
||||
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||
raise HTTPException(
|
||||
HTTP_401_UNAUTHORIZED, "Login by 'Username and Password' not allowed."
|
||||
)
|
||||
|
||||
try:
|
||||
user = await get_account_by_username_or_email(data.username)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Invalid credentials.")
|
||||
if not await verify_user_password(user.id, data.password):
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Invalid credentials.")
|
||||
|
||||
return _auth_success_response(user.username, user.id)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.")
|
||||
|
||||
|
||||
@auth_router.post("/api/v1/auth/usr", description="Login via the User ID")
|
||||
async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'User ID' not allowed.")
|
||||
|
||||
try:
|
||||
user = await get_user(data.usr)
|
||||
if not user:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "User ID does not exist.")
|
||||
|
||||
return _auth_success_response(user.username or "", user.id)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.")
|
||||
|
||||
|
||||
@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_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/{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 provider_sso:
|
||||
userinfo = await provider_sso.verify_and_process(request)
|
||||
assert userinfo is not None
|
||||
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:
|
||||
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(
|
||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
f"Cannot authenticate user with {provider} Auth.",
|
||||
)
|
||||
|
||||
|
||||
@auth_router.post("/api/v1/auth/logout")
|
||||
async def logout() -> JSONResponse:
|
||||
response = JSONResponse({"status": "success"}, status_code=status.HTTP_200_OK)
|
||||
response.delete_cookie("cookie_access_token")
|
||||
response.delete_cookie("is_lnbits_user_authorized")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
response.delete_cookie("lnbits_last_active_wallet")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@auth_router.post("/api/v1/auth/register")
|
||||
async def register(data: CreateUser) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||
raise HTTPException(
|
||||
HTTP_401_UNAUTHORIZED, "Register by 'Username and Password' not allowed."
|
||||
)
|
||||
|
||||
if data.password != data.password_repeat:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Passwords do not match.")
|
||||
|
||||
if not data.username:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Missing username.")
|
||||
if not is_valid_username(data.username):
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid username.")
|
||||
|
||||
if data.email and not is_valid_email_address(data.email):
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
|
||||
|
||||
try:
|
||||
user = await create_user(data)
|
||||
return _auth_success_response(user.username)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot create user.")
|
||||
|
||||
|
||||
@auth_router.put("/api/v1/auth/password")
|
||||
async def update_password(
|
||||
data: UpdateUserPassword, user: User = Depends(check_user_exists)
|
||||
) -> Optional[User]:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||
raise HTTPException(
|
||||
HTTP_401_UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
|
||||
)
|
||||
if data.user_id != user.id:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.")
|
||||
|
||||
try:
|
||||
return await update_user_password(data)
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(
|
||||
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user password."
|
||||
)
|
||||
|
||||
|
||||
@auth_router.put("/api/v1/auth/update")
|
||||
async def update(
|
||||
data: UpdateUser, user: User = Depends(check_user_exists)
|
||||
) -> Optional[User]:
|
||||
if data.user_id != user.id:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.")
|
||||
if data.username and not is_valid_username(data.username):
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid username.")
|
||||
if data.email != user.email:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Email mismatch.")
|
||||
|
||||
try:
|
||||
return await update_account(user.id, data.username, None, data.config)
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user.")
|
||||
|
||||
|
||||
@auth_router.put("/api/v1/auth/first_install")
|
||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||
if not settings.first_install:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "This is not your first install")
|
||||
try:
|
||||
await update_account(
|
||||
user_id=settings.super_user,
|
||||
username=data.username,
|
||||
user_config=UserConfig(provider="lnbits"),
|
||||
)
|
||||
super_user = UpdateUserPassword(
|
||||
user_id=settings.super_user,
|
||||
password=data.password,
|
||||
password_repeat=data.password_repeat,
|
||||
username=data.username,
|
||||
)
|
||||
await update_user_password(super_user)
|
||||
settings.first_install = False
|
||||
return _auth_success_response(username=super_user.username)
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(
|
||||
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user password."
|
||||
)
|
||||
|
||||
|
||||
async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None):
|
||||
email = userinfo.email
|
||||
if not email or not is_valid_email_address(email):
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
|
||||
|
||||
redirect_path = "/wallet"
|
||||
user_config = UserConfig(**dict(userinfo))
|
||||
user_config.email_verified = True
|
||||
|
||||
account = await get_account_by_email(email)
|
||||
|
||||
if verified_user_id:
|
||||
if account:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Email already used.")
|
||||
account = await get_account(verified_user_id)
|
||||
if not account:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Cannot verify user email.")
|
||||
redirect_path = "/account"
|
||||
|
||||
if account:
|
||||
user = await update_account(account.id, email=email, user_config=user_config)
|
||||
else:
|
||||
if not settings.new_accounts_allowed:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.")
|
||||
user = await create_account(email=email, user_config=user_config)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.")
|
||||
|
||||
return _auth_redirect_response(redirect_path, email)
|
||||
|
||||
|
||||
def _auth_success_response(
|
||||
username: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
) -> JSONResponse:
|
||||
access_token = create_access_token(
|
||||
data={"sub": username or "", "usr": user_id, "email": email}
|
||||
)
|
||||
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||
access_token = create_access_token(data={"sub": "" or "", "email": email})
|
||||
response = RedirectResponse(path)
|
||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
return response
|
||||
|
||||
|
||||
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 _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}'.")
|
||||
+130
-55
@@ -1,16 +1,18 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from http import HTTPStatus
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
from typing import Annotated, List, Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import Depends, Query, Request, status
|
||||
from fastapi import Cookie, Depends, Query, Request, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from loguru import logger
|
||||
from pydantic.types import UUID4
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.db import core_app_extra, db
|
||||
from lnbits.core.helpers import to_valid_user_id
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_admin, check_user_exists
|
||||
@@ -19,7 +21,7 @@ from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
from ...extension_manager import InstallableExtension, get_valid_extensions
|
||||
from ...utils.exchange_rates import currencies
|
||||
from ...utils.exchange_rates import allowed_currencies, currencies
|
||||
from ..crud import (
|
||||
create_account,
|
||||
create_wallet,
|
||||
@@ -41,7 +43,7 @@ generic_router = APIRouter(
|
||||
|
||||
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
||||
async def favicon():
|
||||
return FileResponse("lnbits/core/static/favicon.ico")
|
||||
return FileResponse(Path("lnbits", "static", "favicon.ico"))
|
||||
|
||||
|
||||
@generic_router.get("/", response_class=HTMLResponse)
|
||||
@@ -51,6 +53,22 @@ async def home(request: Request, lightning: str = ""):
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/first_install", response_class=HTMLResponse)
|
||||
async def first_install(request: Request):
|
||||
if not settings.first_install:
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html",
|
||||
{
|
||||
"request": request,
|
||||
"err": "Super user account has already been configured.",
|
||||
},
|
||||
)
|
||||
return template_renderer().TemplateResponse(
|
||||
"core/first_install.html",
|
||||
{"request": request},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
||||
async def robots():
|
||||
data = """
|
||||
@@ -73,18 +91,14 @@ async def extensions_install(
|
||||
):
|
||||
await toggle_extension(enable, disable, user.id)
|
||||
|
||||
# Update user as his extensions have been updated
|
||||
if enable or disable:
|
||||
user = await get_user(user.id) # type: ignore
|
||||
try:
|
||||
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:
|
||||
@@ -102,20 +116,28 @@ async def extensions_install(
|
||||
|
||||
try:
|
||||
ext_id = activate or deactivate
|
||||
all_extensions = get_valid_extensions()
|
||||
ext = next((e for e in all_extensions if e.code == ext_id), None)
|
||||
if ext_id and user.admin:
|
||||
if deactivate and deactivate not in settings.lnbits_deactivated_extensions:
|
||||
settings.lnbits_deactivated_extensions += [deactivate]
|
||||
elif activate:
|
||||
# if extension never loaded (was deactivated on server startup)
|
||||
if ext_id not in sys.modules.keys():
|
||||
# run extension start-up routine
|
||||
core_app_extra.register_new_ext_routes(ext)
|
||||
|
||||
settings.lnbits_deactivated_extensions = list(
|
||||
filter(
|
||||
lambda e: e != activate, settings.lnbits_deactivated_extensions
|
||||
)
|
||||
)
|
||||
|
||||
await update_installed_extension_state(
|
||||
ext_id=ext_id, active=activate is not None
|
||||
)
|
||||
|
||||
all_extensions = list(map(lambda e: e.code, get_valid_extensions()))
|
||||
all_ext_ids = list(map(lambda e: e.code, all_extensions))
|
||||
inactive_extensions = await get_inactive_extensions()
|
||||
db_version = await get_dbversions()
|
||||
extensions = list(
|
||||
@@ -130,7 +152,7 @@ async def extensions_install(
|
||||
"dependencies": ext.dependencies,
|
||||
"isInstalled": ext.id in installed_exts_ids,
|
||||
"hasDatabaseTables": ext.id in db_version,
|
||||
"isAvailable": ext.id in all_extensions,
|
||||
"isAvailable": ext.id in all_ext_ids,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"latestRelease": (
|
||||
@@ -144,6 +166,9 @@ async def extensions_install(
|
||||
)
|
||||
)
|
||||
|
||||
# refresh user state. Eg: enabled extensions.
|
||||
user = await get_user(user.id) or user
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
"core/extensions.html",
|
||||
{
|
||||
@@ -164,58 +189,59 @@ async def extensions_install(
|
||||
)
|
||||
async def wallet(
|
||||
request: Request,
|
||||
usr: UUID4 = Query(...),
|
||||
lnbits_last_active_wallet: Annotated[Union[str, None], Cookie()] = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
wal: Optional[UUID4] = Query(None),
|
||||
):
|
||||
user_id = usr.hex
|
||||
user = await get_user(user_id)
|
||||
|
||||
if not user:
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": "User does not exist."}
|
||||
)
|
||||
|
||||
if not wal:
|
||||
if len(user.wallets) == 0:
|
||||
wallet = await create_wallet(user_id=user.id)
|
||||
return RedirectResponse(url=f"/wallet?usr={user_id}&wal={wallet.id}")
|
||||
return RedirectResponse(url=f"/wallet?usr={user_id}&wal={user.wallets[0].id}")
|
||||
else:
|
||||
if wal:
|
||||
wallet_id = wal.hex
|
||||
elif len(user.wallets) == 0:
|
||||
wallet = await create_wallet(user_id=user.id)
|
||||
user = await get_user(user_id=user.id) or user
|
||||
wallet_id = wallet.id
|
||||
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
|
||||
wallet_id = lnbits_last_active_wallet
|
||||
else:
|
||||
wallet_id = user.wallets[0].id
|
||||
|
||||
userwallet = user.get_wallet(wallet_id)
|
||||
if not userwallet or userwallet.deleted:
|
||||
user_wallet = user.get_wallet(wallet_id)
|
||||
if not user_wallet or user_wallet.deleted:
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": "Wallet not found"}
|
||||
)
|
||||
|
||||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
and user_id not in settings.lnbits_allowed_users
|
||||
and user_id not in settings.lnbits_admin_users
|
||||
and user_id != settings.super_user
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": "User not authorized."}
|
||||
)
|
||||
|
||||
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
||||
user.admin = True
|
||||
if user_id == settings.super_user:
|
||||
user.super_user = True
|
||||
|
||||
logger.debug(f"Access user {user.id} wallet {userwallet.name}")
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
resp = template_renderer().TemplateResponse(
|
||||
"core/wallet.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user.dict(),
|
||||
"wallet": userwallet.dict(),
|
||||
"wallet": user_wallet.dict(),
|
||||
"currencies": allowed_currencies(),
|
||||
"service_fee": settings.lnbits_service_fee,
|
||||
"service_fee_max": settings.lnbits_service_fee_max,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
},
|
||||
)
|
||||
resp.set_cookie("lnbits_last_active_wallet", wallet_id)
|
||||
return resp
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/account",
|
||||
response_class=HTMLResponse,
|
||||
description="show account page",
|
||||
)
|
||||
async def account(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
"core/account.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user.dict(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/withdraw", response_class=JSONResponse)
|
||||
@@ -327,9 +353,16 @@ async def lnurlwallet(request: Request):
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/service-worker.js", response_class=FileResponse)
|
||||
async def service_worker():
|
||||
return FileResponse("lnbits/core/static/js/service-worker.js")
|
||||
@generic_router.get("/service-worker.js")
|
||||
async def service_worker(request: Request):
|
||||
return template_renderer().TemplateResponse(
|
||||
"service-worker.js",
|
||||
{
|
||||
"request": request,
|
||||
"cache_version": settings.server_startup_time,
|
||||
},
|
||||
media_type="text/javascript",
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/manifest/{usr}.webmanifest")
|
||||
@@ -350,9 +383,44 @@ async def manifest(request: Request, usr: str):
|
||||
if settings.lnbits_custom_logo
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@main/docs/logos/lnbits.png"
|
||||
),
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"sizes": "900x900",
|
||||
}
|
||||
},
|
||||
{"src": "/static/favicon.ico", "sizes": "32x32", "type": "image/x-icon"},
|
||||
{
|
||||
"src": "/static/images/maskable_icon_x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/static/images/maskable_icon_x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/static/images/maskable_icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "1024x1024",
|
||||
"purpose": "maskable",
|
||||
},
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/static/images/screenshot_desktop.png",
|
||||
"sizes": "2394x1314",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "LNbits - Desktop screenshot",
|
||||
},
|
||||
{
|
||||
"src": "/static/images/screenshot_phone.png",
|
||||
"sizes": "1080x1739",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow",
|
||||
"label": "LNbits - Phone screenshot",
|
||||
},
|
||||
],
|
||||
"start_url": f"/wallet?usr={usr}&wal={user.wallets[0].id}",
|
||||
"background_color": "#1F2234",
|
||||
@@ -366,6 +434,13 @@ async def manifest(request: Request, usr: str):
|
||||
"short_name": wallet.name,
|
||||
"description": wallet.name,
|
||||
"url": f"/wallet?usr={usr}&wal={wallet.id}",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/images/maskable_icon_x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
}
|
||||
],
|
||||
}
|
||||
for wallet in user.wallets
|
||||
],
|
||||
|
||||
@@ -108,9 +108,11 @@ async def api_delete_channel(
|
||||
) -> Optional[List[NodeChannel]]:
|
||||
return await node.close_channel(
|
||||
short_id,
|
||||
ChannelPoint(funding_txid=funding_txid, output_index=output_index)
|
||||
if funding_txid is not None and output_index is not None
|
||||
else None,
|
||||
(
|
||||
ChannelPoint(funding_txid=funding_txid, output_index=output_index)
|
||||
if funding_txid is not None and output_index is not None
|
||||
else None
|
||||
),
|
||||
force,
|
||||
)
|
||||
|
||||
@@ -179,7 +181,8 @@ class NodeRank(BaseModel):
|
||||
)
|
||||
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[NodeRank]:
|
||||
node_id = await node.get_id()
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
r = await client.get(url=f"https://1ml.com/node/{node_id}/json", timeout=15)
|
||||
try:
|
||||
r.raise_for_status()
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
)
|
||||
from starlette.responses import RedirectResponse
|
||||
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
create_tinyurl,
|
||||
delete_tinyurl,
|
||||
get_tinyurl,
|
||||
get_tinyurl_by_url,
|
||||
)
|
||||
|
||||
tinyurl_router = APIRouter()
|
||||
|
||||
|
||||
@tinyurl_router.post(
|
||||
"/api/v1/tinyurl",
|
||||
name="Tinyurl",
|
||||
description="creates a tinyurl",
|
||||
)
|
||||
async def api_create_tinyurl(
|
||||
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
tinyurls = await get_tinyurl_by_url(url)
|
||||
try:
|
||||
for tinyurl in tinyurls:
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.id:
|
||||
return tinyurl
|
||||
return await create_tinyurl(url, endless, wallet.wallet.id)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to create tinyurl"
|
||||
)
|
||||
|
||||
|
||||
@tinyurl_router.get(
|
||||
"/api/v1/tinyurl/{tinyurl_id}",
|
||||
name="Tinyurl",
|
||||
description="get a tinyurl by id",
|
||||
)
|
||||
async def api_get_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.id:
|
||||
return tinyurl
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Unable to fetch tinyurl"
|
||||
)
|
||||
|
||||
|
||||
@tinyurl_router.delete(
|
||||
"/api/v1/tinyurl/{tinyurl_id}",
|
||||
name="Tinyurl",
|
||||
description="delete a tinyurl by id",
|
||||
)
|
||||
async def api_delete_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.id:
|
||||
await delete_tinyurl(tinyurl_id)
|
||||
return {"deleted": True}
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to delete"
|
||||
)
|
||||
|
||||
|
||||
@tinyurl_router.get(
|
||||
"/t/{tinyurl_id}",
|
||||
name="Tinyurl",
|
||||
description="redirects a tinyurl by id",
|
||||
)
|
||||
async def api_tinyurl(tinyurl_id: str):
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
response = RedirectResponse(url=tinyurl.url)
|
||||
return response
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="unable to find tinyurl"
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
import base64
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
Request,
|
||||
)
|
||||
|
||||
from lnbits.core.models import (
|
||||
CreateWebPushSubscription,
|
||||
WebPushSubscription,
|
||||
)
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
require_admin_key,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
create_webpush_subscription,
|
||||
delete_webpush_subscription,
|
||||
get_webpush_subscription,
|
||||
)
|
||||
|
||||
webpush_router = APIRouter(prefix="/api/v1/webpush", tags=["webpush"])
|
||||
|
||||
|
||||
@webpush_router.post("/", status_code=HTTPStatus.CREATED)
|
||||
async def api_create_webpush_subscription(
|
||||
request: Request,
|
||||
data: CreateWebPushSubscription,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> WebPushSubscription:
|
||||
subscription = json.loads(data.subscription)
|
||||
endpoint = subscription["endpoint"]
|
||||
host = urlparse(str(request.url)).netloc
|
||||
|
||||
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
if subscription:
|
||||
return subscription
|
||||
else:
|
||||
return await create_webpush_subscription(
|
||||
endpoint,
|
||||
wallet.wallet.user,
|
||||
data.subscription,
|
||||
host,
|
||||
)
|
||||
|
||||
|
||||
@webpush_router.delete("/", status_code=HTTPStatus.OK)
|
||||
async def api_delete_webpush_subscription(
|
||||
request: Request,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
endpoint = unquote(
|
||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||
)
|
||||
await delete_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
+23
-2
@@ -91,6 +91,12 @@ class Compat:
|
||||
return "(strftime('%s', 'now'))"
|
||||
return "<nothing>"
|
||||
|
||||
@property
|
||||
def timestamp_column_default(self) -> str:
|
||||
if self.type in {POSTGRES, COCKROACH}:
|
||||
return self.timestamp_now
|
||||
return "NULL"
|
||||
|
||||
@property
|
||||
def serial_primary_key(self) -> str:
|
||||
if self.type in {POSTGRES, COCKROACH}:
|
||||
@@ -173,16 +179,27 @@ class Connection(Compat):
|
||||
values: Optional[List[str]] = None,
|
||||
filters: Optional[Filters] = None,
|
||||
model: Optional[Type[TRowModel]] = None,
|
||||
group_by: Optional[List[str]] = None,
|
||||
) -> Page[TRowModel]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
clause = filters.where(where)
|
||||
parsed_values = filters.values(values)
|
||||
|
||||
group_by_string = ""
|
||||
if group_by:
|
||||
for field in group_by:
|
||||
if not re.fullmatch(
|
||||
r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", field
|
||||
):
|
||||
raise ValueError("Value for GROUP BY is invalid")
|
||||
group_by_string = f"GROUP BY {', '.join(group_by)}"
|
||||
|
||||
rows = await self.fetchall(
|
||||
f"""
|
||||
{query}
|
||||
{clause}
|
||||
{group_by_string}
|
||||
{filters.order_by()}
|
||||
{filters.pagination()}
|
||||
""",
|
||||
@@ -196,6 +213,7 @@ class Connection(Compat):
|
||||
SELECT COUNT(*) FROM (
|
||||
{query}
|
||||
{clause}
|
||||
{group_by_string}
|
||||
) as count
|
||||
""",
|
||||
parsed_values,
|
||||
@@ -236,7 +254,9 @@ class Database(Compat):
|
||||
else:
|
||||
self.schema = None
|
||||
|
||||
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
|
||||
self.engine = create_engine(
|
||||
database_uri, strategy=ASYNCIO_STRATEGY, echo=settings.debug_database
|
||||
)
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
logger.trace(f"database {self.type} added for {self.name}")
|
||||
@@ -282,9 +302,10 @@ class Database(Compat):
|
||||
values: Optional[List[str]] = None,
|
||||
filters: Optional[Filters] = None,
|
||||
model: Optional[Type[TRowModel]] = None,
|
||||
group_by: Optional[List[str]] = None,
|
||||
) -> Page[TRowModel]:
|
||||
async with self.connect() as conn:
|
||||
return await conn.fetch_page(query, where, values, filters, model)
|
||||
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
||||
|
||||
async def execute(self, query: str, values: tuple = ()):
|
||||
async with self.connect() as conn:
|
||||
|
||||
+82
-46
@@ -1,21 +1,29 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Literal, Optional, Type
|
||||
from typing import Annotated, Literal, Optional, Type, Union
|
||||
|
||||
from fastapi import Query, Request, Security
|
||||
from fastapi import Cookie, Depends, Query, Request, Security
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security import APIKeyHeader, APIKeyQuery
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
||||
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
|
||||
from fastapi.security.base import SecurityBase
|
||||
from jose import ExpiredSignatureError, JWTError, jwt
|
||||
from loguru import logger
|
||||
from pydantic.types import UUID4
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from lnbits.core.models import User, WalletType, WalletTypeInfo
|
||||
from lnbits.core.crud import (
|
||||
get_account,
|
||||
get_account_by_email,
|
||||
get_account_by_username,
|
||||
get_user,
|
||||
get_wallet_for_key,
|
||||
)
|
||||
from lnbits.core.models import User, Wallet, WalletType, WalletTypeInfo
|
||||
from lnbits.db import Filter, Filters, TFilterModel
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import settings
|
||||
from lnbits.settings import AuthMethods, settings
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth", auto_error=False)
|
||||
|
||||
|
||||
# TODO: fix type ignores
|
||||
class KeyChecker(SecurityBase):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -24,23 +32,25 @@ class KeyChecker(SecurityBase):
|
||||
api_key: Optional[str] = None,
|
||||
):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
self._key_type = WalletType.invoice
|
||||
self.auto_error: bool = auto_error
|
||||
self._key_type: WalletType = WalletType.invoice
|
||||
self._api_key = api_key
|
||||
if api_key:
|
||||
key = APIKey(
|
||||
**{"in": APIKeyIn.query}, # type: ignore
|
||||
openapi_model = APIKey(
|
||||
**{"in": APIKeyIn.query},
|
||||
type=SecuritySchemeType.apiKey,
|
||||
name="X-API-KEY",
|
||||
description="Wallet API Key - QUERY",
|
||||
)
|
||||
else:
|
||||
key = APIKey(
|
||||
**{"in": APIKeyIn.header}, # type: ignore
|
||||
openapi_model = APIKey(
|
||||
**{"in": APIKeyIn.header},
|
||||
type=SecuritySchemeType.apiKey,
|
||||
name="X-API-KEY",
|
||||
description="Wallet API Key - HEADER",
|
||||
)
|
||||
self.wallet = None
|
||||
self.model: APIKey = key
|
||||
self.wallet: Optional[Wallet] = None
|
||||
self.model: APIKey = openapi_model
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
try:
|
||||
@@ -53,12 +63,12 @@ class KeyChecker(SecurityBase):
|
||||
# avoided here. Also, we should not return the wallet here - thats
|
||||
# silly. Possibly store it in a Redis DB
|
||||
wallet = await get_wallet_for_key(key_value, self._key_type)
|
||||
if not wallet or wallet.deleted:
|
||||
if not wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="Invalid key or wallet.",
|
||||
)
|
||||
self.wallet = wallet # type: ignore
|
||||
self.wallet = wallet
|
||||
except KeyError:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="`X-API-KEY` header missing."
|
||||
@@ -220,44 +230,50 @@ async def require_invoice_key(
|
||||
return wallet
|
||||
|
||||
|
||||
async def check_user_exists(usr: UUID4) -> User:
|
||||
g().user = await get_user(usr.hex)
|
||||
|
||||
if not g().user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||
)
|
||||
|
||||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
and g().user.id not in settings.lnbits_allowed_users
|
||||
and g().user.id not in settings.lnbits_admin_users
|
||||
and g().user.id != settings.super_user
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||
)
|
||||
|
||||
return g().user
|
||||
async def check_access_token(
|
||||
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
||||
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
||||
) -> Optional[str]:
|
||||
return header_access_token or cookie_access_token
|
||||
|
||||
|
||||
async def check_admin(usr: UUID4) -> User:
|
||||
user = await check_user_exists(usr)
|
||||
async def check_user_exists(
|
||||
r: Request,
|
||||
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||
usr: Optional[UUID4] = None,
|
||||
) -> User:
|
||||
if access_token:
|
||||
account = await _get_account_from_token(access_token)
|
||||
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||
account = await get_account(usr.hex)
|
||||
else:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing user ID or access token.")
|
||||
|
||||
if not account or not settings.is_user_allowed(account.id):
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.")
|
||||
|
||||
user = await get_user(account.id)
|
||||
assert user, "User not found for account."
|
||||
|
||||
if not user.admin and r["path"].split("/")[1] in settings.lnbits_admin_extensions:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "User not authorized for extension.")
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="User not authorized. No admin privileges.",
|
||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def check_super_user(usr: UUID4) -> User:
|
||||
user = await check_admin(usr)
|
||||
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||
if user.id != settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="User not authorized. No super user privileges.",
|
||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
|
||||
)
|
||||
return user
|
||||
|
||||
@@ -295,3 +311,23 @@ def parse_filters(model: Type[TFilterModel]):
|
||||
)
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
async def _get_account_from_token(access_token):
|
||||
try:
|
||||
payload = jwt.decode(access_token, settings.auth_secret_key, "HS256")
|
||||
if "sub" in payload and payload.get("sub"):
|
||||
return await get_account_by_username(str(payload.get("sub")))
|
||||
if "usr" in payload and payload.get("usr"):
|
||||
return await get_account(str(payload.get("usr")))
|
||||
if "email" in payload and payload.get("email"):
|
||||
return await get_account_by_email(str(payload.get("email")))
|
||||
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Data missing for access token.")
|
||||
except ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
HTTPStatus.UNAUTHORIZED, "Session expired.", {"token-expired": "true"}
|
||||
)
|
||||
except JWTError as e:
|
||||
logger.debug(e)
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid access token.")
|
||||
|
||||
+154
-43
@@ -1,16 +1,15 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Any, List, NamedTuple, Optional, Tuple
|
||||
from urllib import request
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from packaging import version
|
||||
from pydantic import BaseModel
|
||||
@@ -33,11 +32,12 @@ class ExplicitRelease(BaseModel):
|
||||
warning: Optional[str]
|
||||
info_notification: Optional[str]
|
||||
critical_notification: Optional[str]
|
||||
pay_link: Optional[str]
|
||||
|
||||
def is_version_compatible(self):
|
||||
if not self.min_lnbits_version:
|
||||
return True
|
||||
return version.parse(self.min_lnbits_version) <= version.parse(settings.version)
|
||||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
||||
|
||||
|
||||
class GitHubRelease(BaseModel):
|
||||
@@ -75,11 +75,18 @@ class ExtensionConfig(BaseModel):
|
||||
def is_version_compatible(self):
|
||||
if not self.min_lnbits_version:
|
||||
return True
|
||||
return version.parse(self.min_lnbits_version) <= version.parse(settings.version)
|
||||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
||||
|
||||
|
||||
class ReleasePaymentInfo(BaseModel):
|
||||
amount: Optional[int] = None
|
||||
pay_link: Optional[str] = None
|
||||
payment_hash: Optional[str] = None
|
||||
payment_request: Optional[str] = None
|
||||
|
||||
|
||||
def download_url(url, save_path):
|
||||
with request.urlopen(url) as dl_file:
|
||||
with request.urlopen(url, timeout=60) as dl_file:
|
||||
with open(save_path, "wb") as out_file:
|
||||
out_file.write(dl_file.read())
|
||||
|
||||
@@ -144,22 +151,32 @@ async def fetch_github_release_config(
|
||||
|
||||
|
||||
async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = (
|
||||
{"Authorization": "Bearer " + settings.lnbits_ext_github_token}
|
||||
if settings.lnbits_ext_github_token
|
||||
else None
|
||||
)
|
||||
resp = await client.get(
|
||||
url,
|
||||
headers=headers,
|
||||
)
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
if settings.lnbits_ext_github_token:
|
||||
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
resp = await client.get(url)
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"{error_msg} ({url}): {resp.text}")
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def fetch_release_payment_info(
|
||||
url: str, amount: Optional[int] = None
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
if amount:
|
||||
url = f"{url}?amount={amount}"
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
return ReleasePaymentInfo(**resp.json())
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return None
|
||||
|
||||
|
||||
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
@@ -265,6 +282,26 @@ class ExtensionRelease(BaseModel):
|
||||
repo: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
|
||||
pay_link: Optional[str] = None
|
||||
cost_sats: Optional[int] = None
|
||||
paid_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
|
||||
@property
|
||||
def archive_url(self) -> str:
|
||||
if not self.pay_link:
|
||||
return self.archive
|
||||
return (
|
||||
f"{self.archive}?version=v{self.version}&payment_hash={self.payment_hash}"
|
||||
)
|
||||
|
||||
async def check_payment_requirements(self):
|
||||
if not self.pay_link:
|
||||
return
|
||||
|
||||
payment_info = await fetch_release_payment_info(self.pay_link)
|
||||
self.cost_sats = payment_info.amount if payment_info else None
|
||||
|
||||
@classmethod
|
||||
def from_github_release(
|
||||
cls, source_repo: str, r: "GitHubRepoRelease"
|
||||
@@ -295,12 +332,13 @@ class ExtensionRelease(BaseModel):
|
||||
is_version_compatible=e.is_version_compatible(),
|
||||
warning=e.warning,
|
||||
html_url=e.html_url,
|
||||
pay_link=e.pay_link,
|
||||
repo=e.repo,
|
||||
icon=e.icon,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def all_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
|
||||
async def get_github_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
|
||||
try:
|
||||
github_releases = await fetch_github_releases(org, repo)
|
||||
return [
|
||||
@@ -323,6 +361,7 @@ class InstallableExtension(BaseModel):
|
||||
featured = False
|
||||
latest_release: Optional[ExtensionRelease] = None
|
||||
installed_release: Optional[ExtensionRelease] = None
|
||||
payments: List[ReleasePaymentInfo] = []
|
||||
archive: Optional[str] = None
|
||||
|
||||
@property
|
||||
@@ -373,30 +412,32 @@ class InstallableExtension(BaseModel):
|
||||
return self.installed_release.version
|
||||
return ""
|
||||
|
||||
def download_archive(self):
|
||||
async def download_archive(self):
|
||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||
ext_zip_file = self.zip_path
|
||||
if ext_zip_file.is_file():
|
||||
os.remove(ext_zip_file)
|
||||
try:
|
||||
assert self.installed_release, "installed_release is none."
|
||||
download_url(self.installed_release.archive, ext_zip_file)
|
||||
|
||||
self._restore_payment_info()
|
||||
|
||||
await asyncio.to_thread(
|
||||
download_url, self.installed_release.archive_url, ext_zip_file
|
||||
)
|
||||
|
||||
self._remember_payment_info()
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Cannot fetch extension archive file",
|
||||
)
|
||||
raise AssertionError("Cannot fetch extension archive file")
|
||||
|
||||
archive_hash = file_hash(ext_zip_file)
|
||||
if self.installed_release.hash and self.installed_release.hash != archive_hash:
|
||||
# remove downloaded archive
|
||||
if ext_zip_file.is_file():
|
||||
os.remove(ext_zip_file)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="File hash missmatch. Will not install.",
|
||||
)
|
||||
raise AssertionError("File hash missmatch. Will not install.")
|
||||
|
||||
def extract_archive(self):
|
||||
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
|
||||
@@ -470,17 +511,57 @@ class InstallableExtension(BaseModel):
|
||||
if not self.latest_release:
|
||||
self.latest_release = release
|
||||
return
|
||||
if version.parse(self.latest_release.version) < version.parse(release.version):
|
||||
if version_parse(self.latest_release.version) < version_parse(release.version):
|
||||
self.latest_release = release
|
||||
|
||||
def find_existing_payment(
|
||||
self, pay_link: Optional[str]
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
if not pay_link:
|
||||
return None
|
||||
return next(
|
||||
(p for p in self.payments if p.pay_link == pay_link),
|
||||
None,
|
||||
)
|
||||
|
||||
def _restore_payment_info(self):
|
||||
if not self.installed_release:
|
||||
return
|
||||
if not self.installed_release.pay_link:
|
||||
return
|
||||
if self.installed_release.payment_hash:
|
||||
return
|
||||
payment_info = self.find_existing_payment(self.installed_release.pay_link)
|
||||
if payment_info:
|
||||
self.installed_release.payment_hash = payment_info.payment_hash
|
||||
|
||||
def _remember_payment_info(self):
|
||||
if not self.installed_release or not self.installed_release.pay_link:
|
||||
return
|
||||
payment_info = ReleasePaymentInfo(
|
||||
amount=self.installed_release.cost_sats,
|
||||
pay_link=self.installed_release.pay_link,
|
||||
payment_hash=self.installed_release.payment_hash,
|
||||
)
|
||||
self.payments = [
|
||||
p for p in self.payments if p.pay_link != payment_info.pay_link
|
||||
]
|
||||
self.payments.append(payment_info)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, data: dict) -> "InstallableExtension":
|
||||
meta = json.loads(data["meta"])
|
||||
ext = InstallableExtension(**data)
|
||||
if "installed_release" in meta:
|
||||
ext.installed_release = ExtensionRelease(**meta["installed_release"])
|
||||
if meta.get("payments"):
|
||||
ext.payments = [ReleasePaymentInfo(**p) for p in meta["payments"]]
|
||||
return ext
|
||||
|
||||
@classmethod
|
||||
def from_rows(cls, rows: List[Any] = []) -> List["InstallableExtension"]:
|
||||
return [InstallableExtension.from_row(row) for row in rows]
|
||||
|
||||
@classmethod
|
||||
async def from_github_release(
|
||||
cls, github_release: GitHubRelease
|
||||
@@ -570,17 +651,19 @@ class InstallableExtension(BaseModel):
|
||||
try:
|
||||
manifest = await fetch_manifest(url)
|
||||
for r in manifest.repos:
|
||||
if r.id == ext_id:
|
||||
repo_releases = await ExtensionRelease.all_releases(
|
||||
r.organisation, r.repository
|
||||
)
|
||||
extension_releases += repo_releases
|
||||
if r.id != ext_id:
|
||||
continue
|
||||
repo_releases = await ExtensionRelease.get_github_releases(
|
||||
r.organisation, r.repository
|
||||
)
|
||||
extension_releases += repo_releases
|
||||
|
||||
for e in manifest.extensions:
|
||||
if e.id == ext_id:
|
||||
extension_releases += [
|
||||
ExtensionRelease.from_explicit_release(url, e)
|
||||
]
|
||||
if e.id != ext_id:
|
||||
continue
|
||||
explicit_release = ExtensionRelease.from_explicit_release(url, e)
|
||||
await explicit_release.check_payment_requirements()
|
||||
extension_releases.append(explicit_release)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Manifest {url} failed with '{str(e)}'")
|
||||
@@ -589,15 +672,17 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
@classmethod
|
||||
async def get_extension_release(
|
||||
cls, ext_id: str, source_repo: str, archive: str
|
||||
cls, ext_id: str, source_repo: str, archive: str, version: str
|
||||
) -> Optional["ExtensionRelease"]:
|
||||
all_releases: List[
|
||||
ExtensionRelease
|
||||
] = await InstallableExtension.get_extension_releases(ext_id)
|
||||
all_releases: List[ExtensionRelease] = (
|
||||
await InstallableExtension.get_extension_releases(ext_id)
|
||||
)
|
||||
selected_release = [
|
||||
r
|
||||
for r in all_releases
|
||||
if r.archive == archive and r.source_repo == source_repo
|
||||
if r.archive == archive
|
||||
and r.source_repo == source_repo
|
||||
and r.version == version
|
||||
]
|
||||
|
||||
return selected_release[0] if len(selected_release) != 0 else None
|
||||
@@ -607,9 +692,35 @@ class CreateExtension(BaseModel):
|
||||
ext_id: str
|
||||
archive: str
|
||||
source_repo: str
|
||||
version: str
|
||||
cost_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
|
||||
|
||||
def get_valid_extensions() -> List[Extension]:
|
||||
return [
|
||||
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
|
||||
valid_extensions = [
|
||||
extension for extension in ExtensionManager().extensions if extension.is_valid
|
||||
]
|
||||
|
||||
if include_deactivated:
|
||||
return valid_extensions
|
||||
|
||||
if settings.lnbits_extensions_deactivate_all:
|
||||
return []
|
||||
|
||||
return [
|
||||
e
|
||||
for e in valid_extensions
|
||||
if e.code not in settings.lnbits_deactivated_extensions
|
||||
]
|
||||
|
||||
|
||||
def version_parse(v: str):
|
||||
"""
|
||||
Wrapper for version.parse() that does not throw if the version is invalid.
|
||||
Instead it return the lowest possible version ("0.0.0")
|
||||
"""
|
||||
try:
|
||||
return version.parse(v)
|
||||
except Exception:
|
||||
return version.parse("0.0.0")
|
||||
|
||||
+51
-7
@@ -1,9 +1,12 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Type
|
||||
|
||||
import jinja2
|
||||
import shortuuid
|
||||
from jose import jwt
|
||||
from pydantic import BaseModel
|
||||
from pydantic.schema import field_schema
|
||||
|
||||
@@ -11,6 +14,7 @@ from lnbits.jinja2_templating import Jinja2Templates
|
||||
from lnbits.nodes import get_node_class
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
|
||||
from .db import FilterModel
|
||||
from .extension_manager import get_valid_extensions
|
||||
@@ -29,6 +33,10 @@ def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> s
|
||||
return url
|
||||
|
||||
|
||||
def static_url_for(static: str, path: str) -> str:
|
||||
return f"/{static}/{path}?v={settings.server_startup_time}"
|
||||
|
||||
|
||||
def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates:
|
||||
folders = ["lnbits/templates", "lnbits/core/templates"]
|
||||
if additional_folders:
|
||||
@@ -38,6 +46,7 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
|
||||
]
|
||||
folders.extend(additional_folders)
|
||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||
t.env.globals["static_url_for"] = static_url_for
|
||||
|
||||
if settings.lnbits_ad_space_enabled:
|
||||
t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",")
|
||||
@@ -53,22 +62,22 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
|
||||
t.env.globals["LNBITS_QR_LOGO"] = settings.lnbits_qr_logo
|
||||
t.env.globals["LNBITS_VERSION"] = settings.version
|
||||
t.env.globals["LNBITS_NEW_ACCOUNTS_ALLOWED"] = settings.new_accounts_allowed
|
||||
t.env.globals["LNBITS_AUTH_METHODS"] = settings.auth_allowed_methods
|
||||
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
|
||||
t.env.globals["LNBITS_SERVICE_FEE"] = settings.lnbits_service_fee
|
||||
t.env.globals["LNBITS_SERVICE_FEE_MAX"] = settings.lnbits_service_fee_max
|
||||
t.env.globals["LNBITS_SERVICE_FEE_WALLET"] = settings.lnbits_service_fee_wallet
|
||||
t.env.globals["LNBITS_NODE_UI"] = (
|
||||
settings.lnbits_node_ui and get_node_class() is not None
|
||||
)
|
||||
t.env.globals["LNBITS_NODE_UI_AVAILABLE"] = get_node_class() is not None
|
||||
t.env.globals["EXTENSIONS"] = [
|
||||
e
|
||||
for e in get_valid_extensions()
|
||||
if e.code not in settings.lnbits_deactivated_extensions
|
||||
]
|
||||
t.env.globals["EXTENSIONS"] = get_valid_extensions(False)
|
||||
if settings.lnbits_custom_logo:
|
||||
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
|
||||
|
||||
if settings.bundle_assets:
|
||||
t.env.globals["INCLUDED_JS"] = ["/static/bundle.min.js"]
|
||||
t.env.globals["INCLUDED_CSS"] = ["/static/bundle.min.css"]
|
||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||
t.env.globals["INCLUDED_CSS"] = ["bundle.min.css"]
|
||||
else:
|
||||
vendor_filepath = Path(settings.lnbits_path, "static", "vendor.json")
|
||||
with open(vendor_filepath) as vendor_file:
|
||||
@@ -83,6 +92,10 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
|
||||
|
||||
def get_current_extension_name() -> str:
|
||||
"""
|
||||
DEPRECATED: Use the repo name instead, will be removed in the future
|
||||
before: `register_invoice_listener(invoice_queue, get_current_extension_name())`
|
||||
after: `register_invoice_listener(invoice_queue, "my-extension")`
|
||||
|
||||
Returns the name of the extension that calls this method.
|
||||
"""
|
||||
import inspect
|
||||
@@ -158,3 +171,34 @@ def update_query(table_name: str, model: BaseModel, where: str = "WHERE id = ?")
|
||||
"""
|
||||
query = ", ".join([f"{field} = ?" for field in model.dict().keys()])
|
||||
return f"UPDATE {table_name} SET {query} {where}"
|
||||
|
||||
|
||||
def is_valid_email_address(email: str) -> bool:
|
||||
email_regex = r"[A-Za-z0-9\._%+-]+@[A-Za-z0-9\.-]+\.[A-Za-z]{2,63}"
|
||||
return re.fullmatch(email_regex, email) is not None
|
||||
|
||||
|
||||
def is_valid_username(username: str) -> bool:
|
||||
username_regex = r"(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]"
|
||||
return re.fullmatch(username_regex, username) is not None
|
||||
|
||||
|
||||
def create_access_token(data: dict):
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.auth_token_expire_minutes)
|
||||
to_encode = data.copy()
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
||||
|
||||
|
||||
def encrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
||||
"""Encrypt message with the internal secret key"""
|
||||
if not m:
|
||||
return None
|
||||
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode())
|
||||
|
||||
|
||||
def decrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
||||
"""Decrypt message with the internal secret key"""
|
||||
if not m:
|
||||
return None
|
||||
return AESCipher(key=settings.auth_secret_key).decrypt(m)
|
||||
|
||||
@@ -8,8 +8,8 @@ from starlette.templating import Jinja2Templates as SuperJinja2Templates
|
||||
|
||||
class Jinja2Templates(SuperJinja2Templates):
|
||||
def __init__(self, loader: BaseLoader) -> None:
|
||||
super().__init__("")
|
||||
self.env = self.get_environment(loader)
|
||||
super().__init__(env=self.env)
|
||||
|
||||
def get_environment(self, loader: BaseLoader) -> Environment:
|
||||
@pass_context
|
||||
|
||||
+1
-20
@@ -1,20 +1 @@
|
||||
from typing import Union
|
||||
|
||||
from bech32 import bech32_decode, bech32_encode, convertbits
|
||||
from fastapi.datastructures import URL
|
||||
|
||||
|
||||
def decode(lnurl: str) -> str:
|
||||
hrp, data = bech32_decode(lnurl)
|
||||
assert hrp
|
||||
assert data
|
||||
bech32_data = convertbits(data, 5, 8, False)
|
||||
assert bech32_data
|
||||
return bytes(bech32_data).decode()
|
||||
|
||||
|
||||
def encode(url: Union[str, URL]) -> str:
|
||||
bech32_data = convertbits(str(url).encode(), 8, 5, True)
|
||||
assert bech32_data
|
||||
lnurl = bech32_encode("lnurl", bech32_data)
|
||||
return lnurl.upper()
|
||||
from lnurl import LnurlErrorResponse, decode, encode, handle # noqa: F401
|
||||
|
||||
+42
-48
@@ -1,9 +1,8 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Any, List, Tuple, Union
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
@@ -25,67 +24,42 @@ class InstalledExtensionMiddleware:
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if "path" not in scope:
|
||||
full_path = scope.get("path", "/")
|
||||
if full_path == "/":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
path_elements = scope["path"].split("/")
|
||||
if len(path_elements) > 2:
|
||||
_, path_name, path_type, *rest = path_elements
|
||||
else:
|
||||
_, path_name = path_elements
|
||||
path_type = None
|
||||
rest = []
|
||||
|
||||
top_path, *rest = [p for p in full_path.split("/") if p]
|
||||
headers = scope.get("headers", [])
|
||||
|
||||
# block path for all users if the extension is disabled
|
||||
if path_name in settings.lnbits_deactivated_extensions:
|
||||
if top_path in settings.lnbits_deactivated_extensions:
|
||||
response = self._response_by_accepted_type(
|
||||
headers, f"Extension '{path_name}' disabled", HTTPStatus.NOT_FOUND
|
||||
headers, f"Extension '{top_path}' disabled", HTTPStatus.NOT_FOUND
|
||||
)
|
||||
await response(scope, receive, send)
|
||||
return
|
||||
|
||||
if not self._user_allowed_to_extension(path_name, scope):
|
||||
response = self._response_by_accepted_type(
|
||||
headers, "User not authorized.", HTTPStatus.FORBIDDEN
|
||||
)
|
||||
await response(scope, receive, send)
|
||||
# static resources do not require redirect
|
||||
if rest[0:1] == ["static"]:
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
# re-route API trafic if the extension has been upgraded
|
||||
if path_type == "api":
|
||||
upgraded_extensions = list(
|
||||
filter(
|
||||
lambda ext: ext.endswith(f"/{path_name}"),
|
||||
settings.lnbits_upgraded_extensions,
|
||||
)
|
||||
)
|
||||
if len(upgraded_extensions) != 0:
|
||||
upgrade_path = upgraded_extensions[0]
|
||||
tail = "/".join(rest)
|
||||
scope["path"] = f"/upgrades/{upgrade_path}/{path_type}/{tail}"
|
||||
upgrade_path = next(
|
||||
(
|
||||
e
|
||||
for e in settings.lnbits_upgraded_extensions
|
||||
if e.endswith(f"/{top_path}")
|
||||
),
|
||||
None,
|
||||
)
|
||||
# re-route all trafic if the extension has been upgraded
|
||||
if upgrade_path:
|
||||
tail = "/".join(rest)
|
||||
scope["path"] = f"/upgrades/{upgrade_path}/{tail}"
|
||||
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
def _user_allowed_to_extension(self, ext_name: str, scope) -> bool:
|
||||
if ext_name not in settings.lnbits_admin_extensions:
|
||||
return True
|
||||
if "query_string" not in scope:
|
||||
return True
|
||||
|
||||
# parse the URL query string into a `dict`
|
||||
q = parse_qs(scope["query_string"].decode("UTF-8"))
|
||||
user = q.get("usr", [""])[0]
|
||||
if not user:
|
||||
return True
|
||||
|
||||
if user == settings.super_user or user in settings.lnbits_admin_users:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _response_by_accepted_type(
|
||||
self, headers: List[Any], msg: str, status_code: HTTPStatus
|
||||
) -> Union[HTMLResponse, JSONResponse]:
|
||||
@@ -213,7 +187,12 @@ class ExtensionsRedirectMiddleware:
|
||||
|
||||
def add_ratelimit_middleware(app: FastAPI):
|
||||
core_app_extra.register_new_ratelimiter()
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
# latest https://slowapi.readthedocs.io/en/latest/
|
||||
# shows this as a valid way to add the handler
|
||||
app.add_exception_handler(
|
||||
RateLimitExceeded,
|
||||
_rate_limit_exceeded_handler, # type: ignore
|
||||
)
|
||||
app.add_middleware(SlowAPIMiddleware)
|
||||
|
||||
|
||||
@@ -236,3 +215,18 @@ def add_ip_block_middleware(app: FastAPI):
|
||||
return await call_next(request)
|
||||
|
||||
app.middleware("http")(block_allow_ip_middleware)
|
||||
|
||||
|
||||
def add_first_install_middleware(app: FastAPI):
|
||||
@app.middleware("http")
|
||||
async def first_install_middleware(request: Request, call_next):
|
||||
if (
|
||||
settings.first_install
|
||||
and request.url.path != "/api/v1/auth/first_install"
|
||||
and request.url.path != "/first_install"
|
||||
and not request.url.path.startswith("/static")
|
||||
):
|
||||
return RedirectResponse("/first_install")
|
||||
return await call_next(request)
|
||||
|
||||
app.middleware("http")(first_install_middleware)
|
||||
|
||||
+16
-7
@@ -68,13 +68,22 @@ class ChannelStats(BaseModel):
|
||||
if channel.state == ChannelState.ACTIVE
|
||||
]
|
||||
|
||||
return cls(
|
||||
counts=counts,
|
||||
avg_size=int(sum(active_channel_sizes) / len(active_channel_sizes)),
|
||||
biggest_size=max(active_channel_sizes),
|
||||
smallest_size=min(active_channel_sizes),
|
||||
total_capacity=sum(active_channel_sizes),
|
||||
)
|
||||
if len(active_channel_sizes) > 0:
|
||||
return cls(
|
||||
counts=counts,
|
||||
avg_size=int(sum(active_channel_sizes) / len(active_channel_sizes)),
|
||||
biggest_size=max(active_channel_sizes),
|
||||
smallest_size=min(active_channel_sizes),
|
||||
total_capacity=sum(active_channel_sizes),
|
||||
)
|
||||
else:
|
||||
return cls(
|
||||
counts=counts,
|
||||
avg_size=0,
|
||||
biggest_size=0,
|
||||
smallest_size=0,
|
||||
total_capacity=0,
|
||||
)
|
||||
|
||||
|
||||
class NodeFees(BaseModel):
|
||||
|
||||
+24
-13
@@ -217,17 +217,21 @@ class CoreLightningNode(Node):
|
||||
state=(
|
||||
ChannelState.ACTIVE
|
||||
if ch["state"] == "CHANNELD_NORMAL"
|
||||
else ChannelState.PENDING
|
||||
if ch["state"] in ("CHANNELD_AWAITING_LOCKIN", "OPENINGD")
|
||||
else ChannelState.CLOSED
|
||||
if ch["state"]
|
||||
in (
|
||||
"CHANNELD_CLOSING",
|
||||
"CLOSINGD_COMPLETE",
|
||||
"CLOSINGD_SIGEXCHANGE",
|
||||
"ONCHAIN",
|
||||
else (
|
||||
ChannelState.PENDING
|
||||
if ch["state"] in ("CHANNELD_AWAITING_LOCKIN", "OPENINGD")
|
||||
else (
|
||||
ChannelState.CLOSED
|
||||
if ch["state"]
|
||||
in (
|
||||
"CHANNELD_CLOSING",
|
||||
"CLOSINGD_COMPLETE",
|
||||
"CLOSINGD_SIGEXCHANGE",
|
||||
"ONCHAIN",
|
||||
)
|
||||
else ChannelState.INACTIVE
|
||||
)
|
||||
)
|
||||
else ChannelState.INACTIVE
|
||||
),
|
||||
)
|
||||
for ch in funds["channels"]
|
||||
@@ -310,10 +314,17 @@ class CoreLightningNode(Node):
|
||||
return Page(
|
||||
data=[
|
||||
NodeInvoice(
|
||||
bolt11=invoice["bolt11"],
|
||||
amount=invoice["amount_msat"],
|
||||
bolt11=invoice.get("bolt11") or invoice.get("bolt12"),
|
||||
amount=(
|
||||
# normal invoice
|
||||
invoice.get("amount_msat")
|
||||
# keysend or paid amountless invoice
|
||||
or invoice.get("amount_received_msat")
|
||||
# unpaid amountless invoice
|
||||
or 0
|
||||
),
|
||||
preimage=invoice.get("payment_preimage"),
|
||||
memo=invoice["description"],
|
||||
memo=invoice.get("description"),
|
||||
paid_at=invoice.get("paid_at"),
|
||||
expiry=invoice["expires_at"],
|
||||
payment_hash=invoice["payment_hash"],
|
||||
|
||||
+13
-9
@@ -237,9 +237,11 @@ class LndRestNode(Node):
|
||||
remote_msat=msat(channel["remote_balance"]),
|
||||
total_msat=msat(channel["capacity"]),
|
||||
),
|
||||
state=ChannelState.ACTIVE
|
||||
if channel["active"]
|
||||
else ChannelState.INACTIVE,
|
||||
state=(
|
||||
ChannelState.ACTIVE
|
||||
if channel["active"]
|
||||
else ChannelState.INACTIVE
|
||||
),
|
||||
# name=channel['peer_alias'],
|
||||
name=info.alias,
|
||||
color=info.color,
|
||||
@@ -318,11 +320,13 @@ class LndRestNode(Node):
|
||||
amount=payment["value_msat"],
|
||||
fee=payment["fee_msat"],
|
||||
time=payment["creation_date"],
|
||||
destination=await self.get_peer_info(
|
||||
payment["htlcs"][0]["route"]["hops"][-1]["pub_key"]
|
||||
)
|
||||
if payment["htlcs"]
|
||||
else None,
|
||||
destination=(
|
||||
await self.get_peer_info(
|
||||
payment["htlcs"][0]["route"]["hops"][-1]["pub_key"]
|
||||
)
|
||||
if payment["htlcs"]
|
||||
else None
|
||||
),
|
||||
bolt11=payment["payment_request"],
|
||||
preimage=payment["payment_preimage"],
|
||||
)
|
||||
@@ -369,7 +373,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"],
|
||||
)
|
||||
|
||||
+104
-9
@@ -4,8 +4,11 @@ import importlib
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
import json
|
||||
from enum import Enum
|
||||
from hashlib import sha256
|
||||
from os import path
|
||||
from sqlite3 import Row
|
||||
from time import time
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import httpx
|
||||
@@ -65,6 +68,16 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||
# list of redirects that extensions want to perform
|
||||
lnbits_extensions_redirects: List[Any] = Field(default=[])
|
||||
|
||||
def extension_upgrade_path(self, ext_id: str) -> Optional[str]:
|
||||
return next(
|
||||
(e for e in self.lnbits_upgraded_extensions if e.endswith(f"/{ext_id}")),
|
||||
None,
|
||||
)
|
||||
|
||||
def extension_upgrade_hash(self, ext_id: str) -> Optional[str]:
|
||||
path = settings.extension_upgrade_path(ext_id)
|
||||
return path.split("/")[0] if path else None
|
||||
|
||||
|
||||
class ThemesSettings(LNbitsSettings):
|
||||
lnbits_site_title: str = Field(default="LNbits")
|
||||
@@ -98,6 +111,9 @@ class OpsSettings(LNbitsSettings):
|
||||
lnbits_reserve_fee_min: int = Field(default=2000)
|
||||
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
||||
lnbits_service_fee: float = Field(default=0)
|
||||
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
||||
lnbits_service_fee_max: int = Field(default=0)
|
||||
lnbits_service_fee_wallet: str = Field(default=None)
|
||||
lnbits_hide_api: bool = Field(default=False)
|
||||
lnbits_denomination: str = Field(default="sats")
|
||||
|
||||
@@ -110,6 +126,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)
|
||||
@@ -119,6 +138,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")
|
||||
@@ -156,6 +182,7 @@ class LndRestFundingSource(LNbitsSettings):
|
||||
lnd_rest_cert: Optional[str] = Field(default=None)
|
||||
lnd_rest_macaroon: Optional[str] = Field(default=None)
|
||||
lnd_rest_macaroon_encrypted: Optional[str] = Field(default=None)
|
||||
lnd_rest_route_hints: bool = Field(default=True)
|
||||
lnd_cert: Optional[str] = Field(default=None)
|
||||
lnd_admin_macaroon: Optional[str] = Field(default=None)
|
||||
lnd_invoice_macaroon: Optional[str] = Field(default=None)
|
||||
@@ -180,6 +207,16 @@ class LnPayFundingSource(LNbitsSettings):
|
||||
lnpay_admin_key: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class ZBDFundingSource(LNbitsSettings):
|
||||
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
|
||||
zbd_api_key: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class AlbyFundingSource(LNbitsSettings):
|
||||
alby_api_endpoint: Optional[str] = Field(default="https://api.getalby.com/")
|
||||
alby_access_token: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class OpenNodeFundingSource(LNbitsSettings):
|
||||
opennode_api_endpoint: Optional[str] = Field(default=None)
|
||||
opennode_key: Optional[str] = Field(default=None)
|
||||
@@ -199,14 +236,6 @@ class LnTipsFundingSource(LNbitsSettings):
|
||||
lntips_invoice_key: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
# todo: must be extracted
|
||||
class BoltzExtensionSettings(LNbitsSettings):
|
||||
boltz_network: str = Field(default="main")
|
||||
boltz_url: str = Field(default="https://boltz.exchange/api")
|
||||
boltz_mempool_space_url: str = Field(default="https://mempool.space")
|
||||
boltz_mempool_space_url_ws: str = Field(default="wss://mempool.space")
|
||||
|
||||
|
||||
class LightningSettings(LNbitsSettings):
|
||||
lightning_invoice_expiry: int = Field(default=3600)
|
||||
|
||||
@@ -221,6 +250,8 @@ class FundingSourcesSettings(
|
||||
LndRestFundingSource,
|
||||
LndGrpcFundingSource,
|
||||
LnPayFundingSource,
|
||||
AlbyFundingSource,
|
||||
ZBDFundingSource,
|
||||
OpenNodeFundingSource,
|
||||
SparkFundingSource,
|
||||
LnTipsFundingSource,
|
||||
@@ -243,6 +274,44 @@ class NodeUISettings(LNbitsSettings):
|
||||
lnbits_node_ui_transactions: bool = Field(default=False)
|
||||
|
||||
|
||||
class AuthMethods(Enum):
|
||||
user_id_only = "user-id-only"
|
||||
username_and_password = "username-password"
|
||||
google_auth = "google-auth"
|
||||
github_auth = "github-auth"
|
||||
keycloak_auth = "keycloak-auth"
|
||||
|
||||
|
||||
class AuthSettings(LNbitsSettings):
|
||||
auth_token_expire_minutes: int = Field(default=525600)
|
||||
auth_all_methods = [a.value for a in AuthMethods]
|
||||
auth_allowed_methods: List[str] = Field(
|
||||
default=[
|
||||
AuthMethods.user_id_only.value,
|
||||
AuthMethods.username_and_password.value,
|
||||
]
|
||||
)
|
||||
|
||||
def is_auth_method_allowed(self, method: AuthMethods):
|
||||
return method.value in self.auth_allowed_methods
|
||||
|
||||
|
||||
class GoogleAuthSettings(LNbitsSettings):
|
||||
google_client_id: str = Field(default="")
|
||||
google_client_secret: str = Field(default="")
|
||||
|
||||
|
||||
class GitHubAuthSettings(LNbitsSettings):
|
||||
github_client_id: str = Field(default="")
|
||||
github_client_secret: str = Field(default="")
|
||||
|
||||
|
||||
class KeycloakAuthSettings(LNbitsSettings):
|
||||
keycloak_discovery_url: str = Field(default="")
|
||||
keycloak_client_id: str = Field(default="")
|
||||
keycloak_client_secret: str = Field(default="")
|
||||
|
||||
|
||||
class EditableSettings(
|
||||
UsersSettings,
|
||||
ExtensionsSettings,
|
||||
@@ -250,10 +319,13 @@ class EditableSettings(
|
||||
OpsSettings,
|
||||
SecuritySettings,
|
||||
FundingSourcesSettings,
|
||||
BoltzExtensionSettings,
|
||||
LightningSettings,
|
||||
WebPushSettings,
|
||||
NodeUISettings,
|
||||
AuthSettings,
|
||||
GoogleAuthSettings,
|
||||
GitHubAuthSettings,
|
||||
KeycloakAuthSettings,
|
||||
):
|
||||
@validator(
|
||||
"lnbits_admin_users",
|
||||
@@ -287,6 +359,7 @@ class UpdateSettings(EditableSettings):
|
||||
|
||||
class EnvSettings(LNbitsSettings):
|
||||
debug: bool = Field(default=False)
|
||||
debug_database: bool = Field(default=False)
|
||||
bundle_assets: bool = Field(default=True)
|
||||
host: str = Field(default="127.0.0.1")
|
||||
port: int = Field(default=5000)
|
||||
@@ -295,10 +368,15 @@ class EnvSettings(LNbitsSettings):
|
||||
lnbits_path: str = Field(default=".")
|
||||
lnbits_extensions_path: str = Field(default="lnbits")
|
||||
super_user: str = Field(default="")
|
||||
auth_secret_key: str = Field(default="")
|
||||
version: str = Field(default="0.0.0")
|
||||
user_agent: str = Field(default="")
|
||||
enable_log_to_file: bool = Field(default=True)
|
||||
log_rotation: str = Field(default="100 MB")
|
||||
log_retention: str = Field(default="3 months")
|
||||
server_startup_time: int = Field(default=time())
|
||||
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
||||
cleanup_wallets_days: int = Field(default=90)
|
||||
|
||||
@property
|
||||
def has_default_extension_path(self) -> bool:
|
||||
@@ -328,6 +406,8 @@ class SuperUserSettings(LNbitsSettings):
|
||||
"LndWallet",
|
||||
"LnTipsWallet",
|
||||
"LNPayWallet",
|
||||
"AlbyWallet",
|
||||
"ZBDWallet",
|
||||
"LNbitsWallet",
|
||||
"OpenNodeWallet",
|
||||
]
|
||||
@@ -340,6 +420,7 @@ class TransientSettings(InstalledExtensionsSettings):
|
||||
# - are not read from a file or from the `settings` table
|
||||
# - are not persisted in the `settings` table when the settings are updated
|
||||
# - are cleared on server restart
|
||||
first_install: bool = Field(default=False)
|
||||
|
||||
@classmethod
|
||||
def readonly_fields(cls):
|
||||
@@ -380,6 +461,14 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
||||
case_sensitive = False
|
||||
json_loads = list_parse_fallback
|
||||
|
||||
def is_user_allowed(self, user_id: str):
|
||||
return (
|
||||
len(self.lnbits_allowed_users) == 0
|
||||
or user_id in self.lnbits_allowed_users
|
||||
or user_id in self.lnbits_admin_users
|
||||
or user_id == self.super_user
|
||||
)
|
||||
|
||||
|
||||
class SuperSettings(EditableSettings):
|
||||
super_user: str
|
||||
@@ -428,6 +517,12 @@ settings = Settings()
|
||||
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
||||
|
||||
settings.version = importlib.metadata.version("lnbits")
|
||||
settings.auth_secret_key = (
|
||||
settings.auth_secret_key or sha256(settings.super_user.encode("utf-8")).hexdigest()
|
||||
)
|
||||
|
||||
if not settings.user_agent:
|
||||
settings.user_agent = f"LNbits/{settings.version}"
|
||||
|
||||
# printing environment variable for debugging
|
||||
if not settings.lnbits_admin_ui:
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
@@ -502,12 +502,6 @@ video {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Material Icons";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/static/fonts/material-icons-v50.woff2) format("woff2");
|
||||
}
|
||||
.material-icons {
|
||||
font-family: "Material Icons";
|
||||
font-weight: normal;
|
||||
@@ -556,3 +550,7 @@ video {
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.whitespace-pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
+162
-6
@@ -1,8 +1,26 @@
|
||||
window.localisation.br = {
|
||||
confirm: 'Sim',
|
||||
server: 'Servidor',
|
||||
theme: 'Tema',
|
||||
funding: 'Financiamento',
|
||||
users: 'Usuários',
|
||||
apps: 'Aplicativos',
|
||||
channels: 'Canais',
|
||||
transactions: 'Transações',
|
||||
dashboard: 'Painel de Controle',
|
||||
node: 'Nó',
|
||||
total_capacity: 'Capacidade Total',
|
||||
avg_channel_size: 'Tamanho médio do canal',
|
||||
biggest_channel_size: 'Maior Tamanho de Canal',
|
||||
smallest_channel_size: 'Tamanho Mínimo do Canal',
|
||||
number_of_channels: 'Número de Canais',
|
||||
active_channels: 'Canais Ativos',
|
||||
connect_peer: 'Conectar Par',
|
||||
connect: 'Conectar',
|
||||
open_channel: 'Canal Aberto',
|
||||
open: 'Abrir',
|
||||
close_channel: 'Fechar Canal',
|
||||
close: 'Fechar',
|
||||
restart: 'Reiniciar servidor',
|
||||
save: 'Salvar',
|
||||
save_tooltip: 'Salvar suas alterações',
|
||||
@@ -18,7 +36,7 @@ window.localisation.br = {
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, Alby, ZBD, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
export_to_phone: 'Exportar para o telefone com código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
|
||||
@@ -29,27 +47,38 @@ window.localisation.br = {
|
||||
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
|
||||
rename_wallet: 'Renomear carteira',
|
||||
update_name: 'Atualizar nome',
|
||||
fiat_tracking: 'Rastreamento Fiat',
|
||||
currency: 'Moeda',
|
||||
update_currency: 'Atualizar moeda',
|
||||
press_to_claim: 'Pressione para solicitar bitcoin',
|
||||
donate: 'Doar',
|
||||
view_github: 'Ver no GitHub',
|
||||
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
|
||||
use_with_caution: 'USE COM CAUTELA - a carteira %{name} ainda está em BETA',
|
||||
service_fee: 'Taxa de serviço: %{amount} % por transação',
|
||||
service_fee_max:
|
||||
'Taxa de serviço: %{amount} % por transação (máx %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
|
||||
toggle_darkmode: 'Alternar modo escuro',
|
||||
payment_reactions: 'Reações de Pagamento',
|
||||
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
|
||||
api_docs: 'Documentação da API',
|
||||
api_keys_api_docs: 'URL do Node, chaves da API e documentação da API',
|
||||
lnbits_version: 'Versão do LNbits',
|
||||
runs_on: 'Executa em',
|
||||
credit_hint: 'Pressione Enter para creditar a conta',
|
||||
credit_label: '%{denomination} para creditar',
|
||||
paste: 'Colar',
|
||||
paste_from_clipboard: 'Cole do clipboard',
|
||||
paste_request: 'Colar Pedido',
|
||||
create_invoice: 'Criar Fatura',
|
||||
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
|
||||
export_csv: 'Exportar para CSV',
|
||||
transactions: 'Transações',
|
||||
chart_tooltip: 'Mostrar gráfico',
|
||||
pending: 'Pendente',
|
||||
copy_invoice: 'Copiar fatura',
|
||||
close: 'Fechar',
|
||||
withdraw_from: 'Sacar de',
|
||||
cancel: 'Cancelar',
|
||||
scan: 'Escanear',
|
||||
read: 'Ler',
|
||||
@@ -73,17 +102,144 @@ window.localisation.br = {
|
||||
disclaimer_dialog:
|
||||
'Funcionalidade de login a ser lançada em uma atualização futura, por enquanto, certifique-se de marcar esta página para acesso futuro à sua carteira! Este serviço está em BETA, e não nos responsabilizamos por pessoas que perderem o acesso aos fundos.',
|
||||
no_transactions: 'Ainda não foram feitas transações',
|
||||
manage_extensions: 'Gerenciar extensões',
|
||||
manage_server: 'Gerenciar servidor',
|
||||
manage: 'Gerenciar',
|
||||
extensions: 'Extensões',
|
||||
no_extensions: 'Você não possui nenhuma extensão instalada :(',
|
||||
created: 'Criado',
|
||||
search_extensions: 'Extensões de pesquisa',
|
||||
warning: 'Aviso',
|
||||
repository: 'Repositório',
|
||||
confirm_continue: 'Você tem certeza de que deseja continuar?',
|
||||
manage_extension_details: 'Instalar/desinstalar extensão',
|
||||
install: 'Instalar',
|
||||
uninstall: 'Desinstalar',
|
||||
drop_db: 'Remover Dados',
|
||||
enable: 'Ativar',
|
||||
enable_extension_details: 'Ativar extensão para o usuário atual',
|
||||
disable: 'Desativar',
|
||||
installed: 'Instalado',
|
||||
activated: 'Ativado',
|
||||
deactivated: 'Desativado',
|
||||
release_notes: 'Notas de Lançamento',
|
||||
activate_extension_details:
|
||||
'Tornar a extensão disponível/indisponível para usuários',
|
||||
featured: 'Destacado',
|
||||
all: 'Tudo',
|
||||
only_admins_can_install:
|
||||
'Apenas contas de administrador podem instalar extensões.',
|
||||
admin_only: 'Apenas para Administração',
|
||||
new_version: 'Nova Versão',
|
||||
extension_depends_on: 'Depende de:',
|
||||
extension_rating_soon: 'Avaliações estarão disponíveis em breve',
|
||||
extension_installed_version: 'Versão instalada',
|
||||
extension_uninstall_warning:
|
||||
'Você está prestes a remover a extensão para todos os usuários.',
|
||||
uninstall_confirm: 'Sim, Desinstalar',
|
||||
extension_db_drop_info:
|
||||
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
||||
extension_db_drop_warning:
|
||||
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
||||
extension_min_lnbits_version:
|
||||
'Esta versão requer no mínimo a versão do LNbits',
|
||||
payment_hash: 'Hash de pagamento',
|
||||
fee: 'Taxa',
|
||||
amount: 'Quantidade',
|
||||
tag: 'Etiqueta',
|
||||
unit: 'Unidade',
|
||||
description: 'Descrição',
|
||||
expiry: 'Validade',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Comprovante de pagamento'
|
||||
payment_proof: 'Comprovante de pagamento',
|
||||
update_available: 'Atualização %{version} disponível!',
|
||||
latest_update: 'Você está na versão mais recente %{version}.',
|
||||
notifications: 'Notificações',
|
||||
no_notifications: 'Sem notificações',
|
||||
notifications_disabled:
|
||||
'As notificações de status do LNbits estão desativadas.',
|
||||
enable_notifications: 'Ativar notificações',
|
||||
enable_notifications_desc:
|
||||
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
|
||||
enable_killswitch: 'Ativar Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Se ativado, mudará sua fonte de fundos para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
|
||||
killswitch_interval: 'Intervalo do Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNBits proveniente da fonte de status (em minutos).',
|
||||
enable_watchdog: 'Ativar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
|
||||
watchdog_interval: 'Intervalo do Watchdog',
|
||||
watchdog_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar um sinal de interrupção no delta do monitor [node_balance - lnbits_balance] (em minutos).',
|
||||
watchdog_delta: 'Observador Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limite antes da mudança do mecanismo de segurança alterar a fonte de financiamento para VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Estado',
|
||||
notification_source: 'Fonte de Notificação',
|
||||
notification_source_label:
|
||||
'URL de origem (use apenas a fonte de status oficial do LNbits e fontes de confiança)',
|
||||
more: 'mais',
|
||||
less: 'menos',
|
||||
releases: 'Lançamentos',
|
||||
killswitch: 'Dispositivo de desativação',
|
||||
watchdog: 'Cão de guarda',
|
||||
server_logs: 'Registros do Servidor',
|
||||
ip_blocker: 'Bloqueador de IP',
|
||||
security: 'Segurança',
|
||||
security_tools: 'Ferramentas de segurança',
|
||||
block_access_hint: 'Bloquear acesso por IP',
|
||||
allow_access_hint: 'Permitir acesso por IP (substituirá os IPs bloqueados)',
|
||||
enter_ip: 'Digite o IP e pressione enter',
|
||||
rate_limiter: 'Limitador de Taxa',
|
||||
wallet_limiter: 'Limitador de Carteira',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Retirada máxima diária da carteira em sats (0 para desativar)',
|
||||
wallet_max_ballance: 'Saldo máximo da carteira em sats (0 para desativar)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minutos e segundos entre transações por carteira (0 para desativar)',
|
||||
number_of_requests: 'Número de solicitações',
|
||||
time_unit: 'Unidade de tempo',
|
||||
minute: 'minuto',
|
||||
second: 'segundo',
|
||||
hour: 'hora',
|
||||
disable_server_log: 'Desativar Log do Servidor',
|
||||
enable_server_log: 'Ativar Registro do Servidor',
|
||||
coming_soon: 'Funcionalidade em breve',
|
||||
session_has_expired: 'Sua sessão expirou. Por favor, faça login novamente.',
|
||||
instant_access_question: 'Quer acesso imediato?',
|
||||
login_with_user_id: 'Faça login com ID do usuário',
|
||||
or: 'ou',
|
||||
create_new_wallet: 'Criar Nova Carteira',
|
||||
login_to_account: 'Faça login na sua conta',
|
||||
create_account: 'Criar conta',
|
||||
account_settings: 'Configurações da Conta',
|
||||
signin_with_google: 'Entrar com o Google',
|
||||
signin_with_github: 'Entrar com GitHub',
|
||||
signin_with_keycloak: 'Entrar com Keycloak',
|
||||
username_or_email: 'Nome de usuário ou E-mail',
|
||||
password: 'Senha',
|
||||
password_config: 'Configuração de Senha',
|
||||
password_repeat: 'Repetição de senha',
|
||||
change_password: 'Alterar Senha',
|
||||
set_password: 'Definir Senha',
|
||||
invalid_password: 'A senha deve ter pelo menos 8 caracteres',
|
||||
login: 'Entrar',
|
||||
register: 'Registrar',
|
||||
username: 'Nome de usuário',
|
||||
user_id: 'ID do Usuário',
|
||||
email: 'E-mail',
|
||||
first_name: 'Primeiro Nome',
|
||||
last_name: 'Sobrenome',
|
||||
picture: 'Foto',
|
||||
verify_email: 'Verifique o e-mail com',
|
||||
account: 'Conta',
|
||||
update_account: 'Atualizar Conta',
|
||||
invalid_username: 'Nome de usuário inválido',
|
||||
auth_provider: 'Provedor de Autenticação',
|
||||
my_account: 'Minha Conta',
|
||||
back: 'Voltar',
|
||||
logout: 'Sair',
|
||||
look_and_feel: 'Aparência',
|
||||
language: 'Idioma',
|
||||
color_scheme: 'Esquema de Cores'
|
||||
}
|
||||
|
||||
+121
-10
@@ -4,6 +4,23 @@ window.localisation.cn = {
|
||||
theme: '主题',
|
||||
funding: '资金',
|
||||
users: '用户',
|
||||
apps: '应用程序',
|
||||
channels: '频道',
|
||||
transactions: '交易记录',
|
||||
dashboard: '控制面板',
|
||||
node: '节点',
|
||||
total_capacity: '总容量',
|
||||
avg_channel_size: '平均频道大小',
|
||||
biggest_channel_size: '最大通道大小',
|
||||
smallest_channel_size: '最小频道尺寸',
|
||||
number_of_channels: '频道数量',
|
||||
active_channels: '活跃频道',
|
||||
connect_peer: '连接对等',
|
||||
connect: '连接',
|
||||
open_channel: '打开频道',
|
||||
open: '打开',
|
||||
close_channel: '关闭频道',
|
||||
close: '关闭',
|
||||
restart: '重新启动服务器',
|
||||
save: '保存',
|
||||
save_tooltip: '保存更改',
|
||||
@@ -18,7 +35,7 @@ window.localisation.cn = {
|
||||
name_your_wallet: '给你的 %{name}钱包起个名字',
|
||||
paste_invoice_label: '粘贴发票,付款请求或lnurl*',
|
||||
lnbits_description:
|
||||
'LNbits 设置简单、轻量级,可以运行在任何闪电网络的版本上,目前支持 LND、Core Lightning、OpenNode、LNPay,甚至 LNbits 本身!您可以为自己运行 LNbits,或者为他人轻松提供资金托管。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
|
||||
'LNbits 设置简单、轻量级,可以运行在任何闪电网络的版本上,目前支持 LND、Core Lightning、OpenNode、Alby, ZBD, LNPay,甚至 LNbits 本身!您可以为自己运行 LNbits,或者为他人轻松提供资金托管。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
|
||||
export_to_phone: '通过二维码导出到手机',
|
||||
export_to_phone_desc:
|
||||
'这个二维码包含您钱包的URL。您可以使用手机扫描的方式打开您的钱包。',
|
||||
@@ -28,27 +45,36 @@ window.localisation.cn = {
|
||||
delete_wallet_desc: '整个钱包将被删除,资金将无法恢复',
|
||||
rename_wallet: '重命名钱包',
|
||||
update_name: '更新名称',
|
||||
fiat_tracking: '菲亚特追踪',
|
||||
currency: '货币',
|
||||
update_currency: '更新货币',
|
||||
press_to_claim: '点击领取比特币',
|
||||
donate: '捐献',
|
||||
view_github: '在GitHub上查看',
|
||||
voidwallet_active: 'VoidWallet 已激活!付款功能已禁用。',
|
||||
use_with_caution: '请谨慎使用 - %{name}钱包还处于测试版阶段',
|
||||
service_fee: '服务费:%{amount}% 每笔交易',
|
||||
service_fee_max: '服务费:%{amount}% 每笔交易(最高 %{max} sats)',
|
||||
service_fee_tooltip: 'LNbits服务器管理员每笔外发交易收取的服务费',
|
||||
toggle_darkmode: '切换暗黑模式',
|
||||
payment_reactions: '支付反应',
|
||||
view_swagger_docs: '查看 LNbits Swagger API 文档',
|
||||
api_docs: 'API文档',
|
||||
api_keys_api_docs: '节点URL、API密钥和API文档',
|
||||
lnbits_version: 'LNbits版本',
|
||||
runs_on: '可运行在',
|
||||
credit_hint: '按 Enter 键充值账户',
|
||||
credit_label: '%{denomination} 充值',
|
||||
paste: '粘贴',
|
||||
paste_from_clipboard: '从剪贴板粘贴',
|
||||
paste_request: '粘贴请求',
|
||||
create_invoice: '创建发票',
|
||||
camera_tooltip: '用相机扫描发票/二维码',
|
||||
export_csv: '导出为CSV',
|
||||
transactions: '交易记录',
|
||||
chart_tooltip: '显示图表',
|
||||
pending: '待处理',
|
||||
copy_invoice: '复制发票',
|
||||
close: '关闭',
|
||||
withdraw_from: '从',
|
||||
cancel: '取消',
|
||||
scan: '扫描',
|
||||
read: '读取',
|
||||
@@ -72,21 +98,18 @@ window.localisation.cn = {
|
||||
disclaimer_dialog:
|
||||
'登录功能将在以后的更新中发布,请将此页面加为书签,以便将来访问您的钱包!此服务处于测试阶段,我们不对资金的丢失承担任何责任。',
|
||||
no_transactions: '尚未进行任何交易',
|
||||
manage_server: '管理服务器',
|
||||
manage: '管理',
|
||||
extensions: '扩展程序',
|
||||
no_extensions: '你没有安装任何扩展程序 :(',
|
||||
created: '已创建',
|
||||
|
||||
search_extensions: '搜索扩展程序',
|
||||
warning: '警告',
|
||||
manage: '管理',
|
||||
repository: '代码库',
|
||||
confirm_continue: '你确定要继续吗?',
|
||||
manage_extension_details: '安装/卸载扩展程序',
|
||||
install: '安装',
|
||||
uninstall: '卸载',
|
||||
drop_db: '删除数据',
|
||||
open: '打开',
|
||||
enable: '启用',
|
||||
enable_extension_details: '为当前用户启用扩展程序',
|
||||
disable: '禁用',
|
||||
@@ -108,15 +131,103 @@ window.localisation.cn = {
|
||||
extension_db_drop_info: '该扩展程序的所有数据将被永久删除。此操作无法撤销!',
|
||||
extension_db_drop_warning:
|
||||
'您即将删除该扩展的所有数据。请继续输入扩展程序名称以确认操作:',
|
||||
|
||||
extension_min_lnbits_version: '此版本要求最低的 LNbits 版本为',
|
||||
|
||||
payment_hash: '付款哈希',
|
||||
fee: '费',
|
||||
amount: '金额',
|
||||
tag: '标签',
|
||||
unit: '单位',
|
||||
description: '详情',
|
||||
expiry: '过期时间',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: '付款证明'
|
||||
payment_proof: '付款证明',
|
||||
update_available: '更新%{version}可用!',
|
||||
latest_update: '您当前使用的是最新版本%{version}。',
|
||||
notifications: '通知',
|
||||
no_notifications: '没有通知',
|
||||
notifications_disabled: 'LNbits状态通知已禁用。',
|
||||
enable_notifications: '启用通知',
|
||||
enable_notifications_desc:
|
||||
'如果启用,它将获取最新的LNbits状态更新,如安全事件和更新。',
|
||||
enable_killswitch: '启用紧急停止开关',
|
||||
enable_killswitch_desc:
|
||||
'如果启用,当LNbits发送终止信号时,系统将自动将您的资金来源更改为VoidWallet。更新后,您将需要手动启用。',
|
||||
killswitch_interval: 'Killswitch 间隔',
|
||||
killswitch_interval_desc:
|
||||
'后台任务应该多久检查一次来自状态源的LNBits断路信号(以分钟为单位)。',
|
||||
enable_watchdog: '启用看门狗',
|
||||
enable_watchdog_desc:
|
||||
'如果启用,当您的余额低于LNbits余额时,系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。',
|
||||
watchdog_interval: '看门狗间隔',
|
||||
watchdog_interval_desc:
|
||||
'后台任务应该多久检查一次看门狗增量中的 killswitch 信号 [node_balance - lnbits_balance](以分钟计)。',
|
||||
watchdog_delta: '看门狗德尔塔',
|
||||
watchdog_delta_desc:
|
||||
'在触发紧急停止前切换资金来源至VoidWallet的限制 [lnbits_balance - node_balance > delta]',
|
||||
status: '状态',
|
||||
notification_source: '通知来源',
|
||||
notification_source_label: '来源 URL(仅使用官方LNbits状态源和您信任的源)',
|
||||
more: '更多',
|
||||
less: '少',
|
||||
releases: '版本',
|
||||
killswitch: '杀手锏',
|
||||
watchdog: '监控程序',
|
||||
server_logs: '服务器日志',
|
||||
ip_blocker: 'IP 阻止器',
|
||||
security: '安全',
|
||||
security_tools: '安全工具',
|
||||
block_access_hint: '屏蔽IP访问',
|
||||
allow_access_hint: '允许通过IP访问(将覆盖被屏蔽的IP)',
|
||||
enter_ip: '输入IP地址并按回车键',
|
||||
rate_limiter: '速率限制器',
|
||||
wallet_limiter: '钱包限制器',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'每日钱包最大提现额度(单位:sats)(设为0则禁用)',
|
||||
wallet_max_ballance: '钱包最大余额(以sats计)(设为0则禁用)',
|
||||
wallet_limit_secs_between_trans: '每个钱包交易间最少秒数(设为0则禁用)',
|
||||
number_of_requests: '请求次数',
|
||||
time_unit: '时间单位',
|
||||
minute: '分钟',
|
||||
second: '秒',
|
||||
hour: '小时',
|
||||
disable_server_log: '禁用服务器日志',
|
||||
enable_server_log: '启用服务器日志',
|
||||
coming_soon: '功能即将推出',
|
||||
session_has_expired: '您的会话已过期。请重新登录。',
|
||||
instant_access_question: '想要即时访问吗?',
|
||||
login_with_user_id: '使用用户ID登录',
|
||||
or: '或',
|
||||
create_new_wallet: '创建新钱包',
|
||||
login_to_account: '登录您的账户',
|
||||
create_account: '创建账户',
|
||||
account_settings: '账户设置',
|
||||
signin_with_google: '使用谷歌账号登录',
|
||||
signin_with_github: '使用GitHub登录',
|
||||
signin_with_keycloak: '使用Keycloak登录',
|
||||
username_or_email: '用户名或电子邮箱',
|
||||
password: '密码',
|
||||
password_config: '密码配置',
|
||||
password_repeat: '密码重复',
|
||||
change_password: '修改密码',
|
||||
set_password: '设置密码',
|
||||
invalid_password: '密码至少需要有8个字符',
|
||||
login: '登录',
|
||||
register: '注册',
|
||||
username: '用户名',
|
||||
user_id: '用户ID',
|
||||
email: '电子邮件',
|
||||
first_name: '名字',
|
||||
last_name: '姓氏',
|
||||
picture: '图片',
|
||||
verify_email: '验证电子邮件与',
|
||||
account: '账户',
|
||||
update_account: '更新帐户',
|
||||
invalid_username: '无效用户名',
|
||||
auth_provider: '认证提供者',
|
||||
my_account: '我的账户',
|
||||
back: '返回',
|
||||
logout: '注销',
|
||||
look_and_feel: '外观和感觉',
|
||||
language: '语言',
|
||||
color_scheme: '配色方案'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
window.localisation.cs = {
|
||||
confirm: 'Ano',
|
||||
server: 'Server',
|
||||
theme: 'Téma',
|
||||
funding: 'Financování',
|
||||
users: 'Uživatelé',
|
||||
apps: 'Aplikace',
|
||||
channels: 'Kanály',
|
||||
transactions: 'Transakce',
|
||||
dashboard: 'Přehled',
|
||||
node: 'Uzel',
|
||||
total_capacity: 'Celková kapacita',
|
||||
avg_channel_size: 'Průmerná velikost kanálu',
|
||||
biggest_channel_size: 'Největší velikost kanálu',
|
||||
smallest_channel_size: 'Nejmenší velikost kanálu',
|
||||
number_of_channels: 'Počet kanálů',
|
||||
active_channels: 'Aktivní kanály',
|
||||
connect_peer: 'Připojit peer',
|
||||
connect: 'Připojit',
|
||||
open_channel: 'Otevřít kanál',
|
||||
open: 'Otevřít',
|
||||
close_channel: 'Zavřít kanál',
|
||||
close: 'Zavřít',
|
||||
restart: 'Restartovat server',
|
||||
save: 'Uložit',
|
||||
save_tooltip: 'Uložit změny',
|
||||
topup: 'Dobít',
|
||||
topup_wallet: 'Dobít peněženku',
|
||||
topup_hint: 'Použijte ID peněženky pro dobíjení jakékoliv peněženky',
|
||||
restart_tooltip: 'Restartujte server pro aplikaci změn',
|
||||
add_funds_tooltip: 'Přidat prostředky do peněženky.',
|
||||
reset_defaults: 'Obnovit výchozí',
|
||||
reset_defaults_tooltip: 'Smazat všechna nastavení a obnovit výchozí.',
|
||||
download_backup: 'Stáhnout zálohu databáze',
|
||||
name_your_wallet: 'Pojmenujte svou %{name} peněženku',
|
||||
paste_invoice_label: 'Vložte fakturu, platební požadavek nebo lnurl kód *',
|
||||
lnbits_description:
|
||||
'Snadno nastavitelný a lehkotonážní, LNbits může běžet na jakémkoliv zdroji financování lightning-network, v současné době podporuje LND, Core Lightning, OpenNode, Alby, ZBD, LNPay a dokonce LNbits samotné! LNbits můžete provozovat pro sebe, nebo snadno nabízet správu peněženek pro ostatní. Každá peněženka má své vlastní API klíče a není omezen počet peněženek, které můžete vytvořit. Možnost rozdělení prostředků dělá z LNbits užitečný nástroj pro správu peněz a jako vývojový nástroj. Rozšíření přidávají extra funkčnost k LNbits, takže můžete experimentovat s řadou špičkových technologií na lightning network. Vývoj rozšíření jsme učinili co nejjednodušší a jako svobodný a open-source projekt podporujeme lidi ve vývoji a zasílání vlastních rozšíření.',
|
||||
export_to_phone: 'Exportovat do telefonu pomocí QR kódu',
|
||||
export_to_phone_desc:
|
||||
'Tento QR kód obsahuje URL vaší peněženky s plným přístupem. Můžete jej naskenovat z telefonu a otevřít peněženku odtamtud.',
|
||||
wallets: 'Peněženky',
|
||||
add_wallet: 'Přidat novou peněženku',
|
||||
delete_wallet: 'Smazat peněženku',
|
||||
delete_wallet_desc:
|
||||
'Celá peněženka bude smazána, prostředky budou NEOBNOVITELNÉ.',
|
||||
rename_wallet: 'Přejmenovat peněženku',
|
||||
update_name: 'Aktualizovat název',
|
||||
fiat_tracking: 'Sledování fiatu',
|
||||
currency: 'Měna',
|
||||
update_currency: 'Aktualizovat měnu',
|
||||
press_to_claim: 'Stiskněte pro nárokování bitcoinu',
|
||||
donate: 'Darovat',
|
||||
view_github: 'Zobrazit na GitHubu',
|
||||
voidwallet_active: 'VoidWallet je aktivní! Platby zakázány',
|
||||
use_with_caution:
|
||||
'POUŽÍVEJTE S OBEZŘETNOSTÍ - %{name} peněženka je stále v BETĚ',
|
||||
service_fee: 'Servisný poplatek: %{amount} % za transakci',
|
||||
service_fee_max:
|
||||
'Servisný poplatek: %{amount} % za transakci (max %{max} satoshi)',
|
||||
service_fee_tooltip:
|
||||
'Servisní poplatek účtovaný správcem LNbits serveru za odchozí transakci',
|
||||
toggle_darkmode: 'Přepnout tmavý režim',
|
||||
payment_reactions: 'Reakce na platby',
|
||||
view_swagger_docs: 'Zobrazit LNbits Swagger API dokumentaci',
|
||||
api_docs: 'API dokumentace',
|
||||
api_keys_api_docs: 'Adresa uzlu, API klíče a API dokumentace',
|
||||
lnbits_version: 'Verze LNbits',
|
||||
runs_on: 'Běží na',
|
||||
credit_hint: 'Stiskněte Enter pro připsání na účet',
|
||||
credit_label: '%{denomination} k připsání',
|
||||
paste: 'Vložit',
|
||||
paste_from_clipboard: 'Vložit ze schránky',
|
||||
paste_request: 'Vložit požadavek',
|
||||
create_invoice: 'Vytvořit fakturu',
|
||||
camera_tooltip: 'Použijte kameru pro skenování faktury/QR',
|
||||
export_csv: 'Exportovat do CSV',
|
||||
chart_tooltip: 'Zobrazit graf',
|
||||
pending: 'Čeká na vyřízení',
|
||||
copy_invoice: 'Kopírovat fakturu',
|
||||
withdraw_from: 'Vybrat z',
|
||||
cancel: 'Zrušit',
|
||||
scan: 'Skenovat',
|
||||
read: 'Číst',
|
||||
pay: 'Platit',
|
||||
memo: 'Poznámka',
|
||||
date: 'Datum',
|
||||
processing_payment: 'Zpracování platby...',
|
||||
not_enough_funds: 'Nedostatek prostředků!',
|
||||
search_by_tag_memo_amount: 'Hledat podle tagu, poznámky, částky',
|
||||
invoice_waiting: 'Faktura čeká na platbu',
|
||||
payment_received: 'Platba přijata',
|
||||
payment_sent: 'Platba odeslána',
|
||||
receive: 'přijmout',
|
||||
send: 'odeslat',
|
||||
outgoing_payment_pending: 'Odchozí platba čeká na vyřízení',
|
||||
drain_funds: 'Vyčerpat prostředky',
|
||||
drain_funds_desc:
|
||||
'Toto je LNURL-withdraw QR kód pro vyčerpání všeho z této peněženky. Nesdílejte s nikým. Je kompatibilní s balanceCheck a balanceNotify, takže vaše peněženka může kontinuálně čerpat prostředky odsud po prvním výběru.',
|
||||
i_understand: 'Rozumím',
|
||||
copy_wallet_url: 'Kopírovat URL peněženky',
|
||||
disclaimer_dialog:
|
||||
'Funkcionalita přihlášení bude vydána v budoucí aktualizaci, zatím si ujistěte, že jste si tuto stránku uložili do záložek pro budoucí přístup k vaší peněžence! Tato služba je v BETA verzi a nepřebíráme žádnou zodpovědnost za ztrátu přístupu k prostředkům.',
|
||||
no_transactions: 'Zatím žádné transakce',
|
||||
manage: 'Spravovat',
|
||||
extensions: 'Rozšíření',
|
||||
no_extensions: 'Nemáte nainstalováno žádné rozšíření :(',
|
||||
created: 'Vytvořeno',
|
||||
search_extensions: 'Hledat rozšíření',
|
||||
warning: 'Varování',
|
||||
repository: 'Repositář',
|
||||
confirm_continue: 'Jste si jistí, že chcete pokračovat?',
|
||||
manage_extension_details: 'Instalovat/odinstalovat rozšíření',
|
||||
install: 'Instalovat',
|
||||
uninstall: 'Odinstalovat',
|
||||
drop_db: 'Odstranit data',
|
||||
enable: 'Povolit',
|
||||
enable_extension_details: 'Povolit rozšíření pro aktuálního uživatele',
|
||||
disable: 'Zakázat',
|
||||
installed: 'Nainstalováno',
|
||||
activated: 'Aktivováno',
|
||||
deactivated: 'Deaktivováno',
|
||||
release_notes: 'Poznámky k vydání',
|
||||
activate_extension_details: 'Zpřístupnit/zakázat rozšíření pro uživatele',
|
||||
featured: 'Doporučené',
|
||||
all: 'Vše',
|
||||
only_admins_can_install:
|
||||
'(Pouze administrátorské účty mohou instalovat rozšíření)',
|
||||
admin_only: 'Pouze pro adminy',
|
||||
new_version: 'Nová verze',
|
||||
extension_depends_on: 'Závisí na:',
|
||||
extension_rating_soon: 'Hodnocení brzy dostupné',
|
||||
extension_installed_version: 'Nainstalovaná verze',
|
||||
extension_uninstall_warning:
|
||||
'Chystáte se odstranit rozšíření pro všechny uživatele.',
|
||||
uninstall_confirm: 'Ano, odinstalovat',
|
||||
extension_db_drop_info:
|
||||
'Všechna data pro rozšíření budou trvale odstraněna. Tuto operaci nelze vrátit zpět!',
|
||||
extension_db_drop_warning:
|
||||
'Chystáte se odstranit všechna data pro rozšíření. Prosím, pokračujte zadáním názvu rozšíření:',
|
||||
extension_min_lnbits_version: 'Toto vydání vyžaduje alespoň verzi LNbits',
|
||||
payment_hash: 'Hash platby',
|
||||
fee: 'Poplatek',
|
||||
amount: 'Částka',
|
||||
tag: 'Tag',
|
||||
unit: 'Jednotka',
|
||||
description: 'Popis',
|
||||
expiry: 'Expirace',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Důkaz platby',
|
||||
update_available: 'Dostupná aktualizace %{version}!',
|
||||
latest_update: 'Máte nejnovější verzi %{version}.',
|
||||
notifications: 'Notifikace',
|
||||
no_notifications: 'Žádné notifikace',
|
||||
notifications_disabled: 'Notifikace stavu LNbits jsou zakázány.',
|
||||
enable_notifications: 'Povolit notifikace',
|
||||
enable_notifications_desc:
|
||||
'Pokud je povoleno, bude stahovat nejnovější aktualizace stavu LNbits, jako jsou bezpečnostní incidenty a aktualizace.',
|
||||
enable_killswitch: 'Povolit Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud LNbits odešle signál killswitch. Po aktualizaci budete muset povolit ručně.',
|
||||
killswitch_interval: 'Interval Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNBits ze zdroje stavu (v minutách).',
|
||||
enable_watchdog: 'Povolit Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud je váš zůstatek nižší než zůstatek LNbits. Po aktualizaci budete muset povolit ručně.',
|
||||
watchdog_interval: 'Interval Watchdog',
|
||||
watchdog_interval_desc:
|
||||
'Jak často by měl úkol na pozadí kontrolovat signál killswitch v watchdog delta [node_balance - lnbits_balance] (v minutách).',
|
||||
watchdog_delta: 'Delta Watchdog',
|
||||
watchdog_delta_desc:
|
||||
'Limit předtím, než killswitch změní zdroj financování na VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Stav',
|
||||
notification_source: 'Zdroj notifikací',
|
||||
notification_source_label:
|
||||
'URL zdroje (používejte pouze oficiální zdroj stavu LNbits a zdroje, kterým můžete věřit)',
|
||||
more: 'více',
|
||||
less: 'méně',
|
||||
releases: 'Vydání',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Logy serveru',
|
||||
ip_blocker: 'Blokování IP',
|
||||
security: 'Bezpečnost',
|
||||
security_tools: 'Nástroje bezpečnosti',
|
||||
block_access_hint: 'Blokovat přístup podle IP',
|
||||
allow_access_hint: 'Povolit přístup podle IP (přepíše blokované IP)',
|
||||
enter_ip: 'Zadejte IP a stiskněte enter',
|
||||
rate_limiter: 'Omezovač počtu požadavků',
|
||||
wallet_limiter: 'Omezení peněženky',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximální denní limit pro výběr z peněženky v sats (0 pro deaktivaci)',
|
||||
wallet_max_ballance: 'Maximální zůstatek v peněžence v sats (0 pro zakázání)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minimální počet sekund mezi transakcemi na peněženku (0 pro vypnutí)',
|
||||
number_of_requests: 'Počet požadavků',
|
||||
time_unit: 'Časová jednotka',
|
||||
minute: 'minuta',
|
||||
second: 'sekunda',
|
||||
hour: 'hodina',
|
||||
disable_server_log: 'Zakázat log serveru',
|
||||
enable_server_log: 'Povolit log serveru',
|
||||
coming_soon: 'Funkce brzy dostupná',
|
||||
session_has_expired: 'Vaše relace vypršela. Prosím, přihlašte se znovu.',
|
||||
instant_access_question: 'Chcete okamžitý přístup?',
|
||||
login_with_user_id: 'Přihlásit se s uživatelským ID',
|
||||
or: 'nebo',
|
||||
create_new_wallet: 'Vytvořit novou peněženku',
|
||||
login_to_account: 'Přihlaste se ke svému účtu',
|
||||
create_account: 'Vytvořit účet',
|
||||
account_settings: 'Nastavení účtu',
|
||||
signin_with_google: 'Přihlásit se přes Google',
|
||||
signin_with_github: 'Přihlásit se přes GitHub',
|
||||
signin_with_keycloak: 'Přihlásit se přes Keycloak',
|
||||
username_or_email: 'Uživatelské jméno nebo Email',
|
||||
password: 'Heslo',
|
||||
password_config: 'Konfigurace hesla',
|
||||
password_repeat: 'Opakujte heslo',
|
||||
change_password: 'Změnit heslo',
|
||||
set_password: 'Nastavit heslo',
|
||||
invalid_password: 'Heslo musí mít alespoň 8 znaků',
|
||||
login: 'Přihlášení',
|
||||
register: 'Registrovat',
|
||||
username: 'Uživatelské jméno',
|
||||
user_id: 'ID uživatele',
|
||||
email: 'Email',
|
||||
first_name: 'Křestní jméno',
|
||||
last_name: 'Příjmení',
|
||||
picture: 'Obrázek',
|
||||
verify_email: 'Ověřte e-mail s',
|
||||
account: 'Účet',
|
||||
update_account: 'Aktualizovat účet',
|
||||
invalid_username: 'Neplatné uživatelské jméno',
|
||||
auth_provider: 'Poskytovatel ověření',
|
||||
my_account: 'Můj účet',
|
||||
back: 'Zpět',
|
||||
logout: 'Odhlásit se',
|
||||
look_and_feel: 'Vzhled a chování',
|
||||
language: 'Jazyk',
|
||||
color_scheme: 'Barevné schéma'
|
||||
}
|
||||
+138
-13
@@ -1,9 +1,26 @@
|
||||
window.localisation.de = {
|
||||
confirm: 'Ja',
|
||||
server: 'Server',
|
||||
theme: 'Theme',
|
||||
funding: 'Funding',
|
||||
users: 'Benutzer',
|
||||
unit: 'Einheit',
|
||||
apps: 'Apps',
|
||||
channels: 'Kanäle',
|
||||
transactions: 'Transaktionen',
|
||||
dashboard: 'Armaturenbrett',
|
||||
node: 'Knoten',
|
||||
total_capacity: 'Gesamtkapazität',
|
||||
avg_channel_size: 'Durchschn. Kanalgröße',
|
||||
biggest_channel_size: 'Größte Kanalgröße',
|
||||
smallest_channel_size: 'Kleinste Kanalgröße',
|
||||
number_of_channels: 'Anzahl der Kanäle',
|
||||
active_channels: 'Aktive Kanäle',
|
||||
connect_peer: 'Peer verbinden',
|
||||
connect: 'Verbinden',
|
||||
open_channel: 'Offener Kanal',
|
||||
open: 'Öffnen',
|
||||
close_channel: 'Kanal schließen',
|
||||
close: 'Schließen',
|
||||
restart: 'Server neu starten',
|
||||
save: 'Speichern',
|
||||
save_tooltip: 'Änderungen speichern',
|
||||
@@ -20,7 +37,7 @@ window.localisation.de = {
|
||||
paste_invoice_label:
|
||||
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
|
||||
lnbits_description:
|
||||
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, Core Lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
|
||||
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, Core Lightning, OpenNode, Alby, ZBD, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
|
||||
export_to_phone: 'Auf dem Telefon öffnen',
|
||||
export_to_phone_desc:
|
||||
'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannst den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
|
||||
@@ -31,28 +48,40 @@ window.localisation.de = {
|
||||
'Die Wallet wird gelöscht, die hierin beinhalteten Daten hierin oder innerhalb einer Erweiterung sind UNWIEDERBRINGLICH.',
|
||||
rename_wallet: 'Wallet umbenennen',
|
||||
update_name: 'Namen aktualisieren',
|
||||
fiat_tracking: 'Fiat-Tracking',
|
||||
currency: 'Währung',
|
||||
update_currency: 'Währung aktualisieren',
|
||||
press_to_claim: 'Klicken, um Bitcoin einzufordern.',
|
||||
donate: 'Spenden',
|
||||
view_github: 'Auf GitHub anzeigen',
|
||||
voidwallet_active: 'VoidWallet ist aktiv! Zahlungen deaktiviert',
|
||||
use_with_caution:
|
||||
'BITTE MIT VORSICHT BENUTZEN - %{name} Wallet ist noch BETA',
|
||||
service_fee: 'Dienstleistungsgebühr: %{amount} % pro Transaktion',
|
||||
service_fee_max:
|
||||
'Servicegebühr: %{amount} % pro Transaktion (max %{max} Sats)',
|
||||
service_fee_tooltip:
|
||||
'Bearbeitungsgebühr, die vom LNbits Server-Administrator pro ausgehender Transaktion berechnet wird',
|
||||
toggle_darkmode: 'Auf Dark Mode umschalten',
|
||||
view_swagger_docs: 'LNbits Swagger API-Dokumente',
|
||||
api_docs: 'API docs',
|
||||
payment_reactions: 'Zahlungsreaktionen',
|
||||
view_swagger_docs: 'LNbits Swagger API-Dokumentation',
|
||||
api_docs: 'API-Dokumentation',
|
||||
api_keys_api_docs: 'Knoten-URL, API-Schlüssel und API-Dokumentation',
|
||||
lnbits_version: 'LNbits-Version',
|
||||
runs_on: 'Läuft auf',
|
||||
credit_hint: 'Klicke Enter, um das Konto zu belasten',
|
||||
credit_label: '%{denomination} zu belasten',
|
||||
paste: 'Einfügen',
|
||||
paste_from_clipboard: 'Einfügen aus der Zwischenablage',
|
||||
paste_request: 'Anfrage einfügen',
|
||||
create_invoice: 'Rechnung erstellen',
|
||||
camera_tooltip:
|
||||
'Verwende die Kamera, um eine Rechnung oder einen QR-Code zu scannen',
|
||||
export_csv: 'Exportieren als CSV',
|
||||
transactions: 'Transaktionen',
|
||||
chart_tooltip: 'Diagramm anzeigen',
|
||||
pending: 'Ausstehend',
|
||||
copy_invoice: 'Rechnung kopieren',
|
||||
close: 'Schließen',
|
||||
withdraw_from: 'Abheben von',
|
||||
cancel: 'Stornieren',
|
||||
scan: 'Scannen',
|
||||
read: 'Lesen',
|
||||
@@ -76,21 +105,18 @@ window.localisation.de = {
|
||||
disclaimer_dialog:
|
||||
'Login-Funktionalität wird in einem zukünftigen Update veröffentlicht. Bis dahin ist die Speicherung der Wallet-URL als Lesezeichen absolut notwendig, um Zugriff auf die Wallet zu erhalten! Dieser Service ist in BETA und wir übernehmen keine Verantwortung für Verluste durch verlorene Zugriffe.',
|
||||
no_transactions: 'Keine Transaktionen',
|
||||
manage_extensions: 'Erweiterungen verwalten',
|
||||
manage_server: 'Server verwalten',
|
||||
manage: 'Verwalten',
|
||||
extensions: 'Erweiterungen',
|
||||
no_extensions: 'Du hast noch keine Erweiterungen installiert :(',
|
||||
created: 'Erstellt',
|
||||
|
||||
search_extensions: 'Sucherweiterungen',
|
||||
warning: 'Warnung',
|
||||
manage: 'Verwalten',
|
||||
repository: 'Repository',
|
||||
confirm_continue: 'Bist du sicher, dass du fortfahren möchtest?',
|
||||
manage_extension_details: 'Erweiterung installieren/deinstallieren',
|
||||
install: 'Installieren',
|
||||
uninstall: 'Deinstallieren',
|
||||
open: 'Öffnen',
|
||||
drop_db: 'Daten löschen',
|
||||
enable: 'Aktivieren',
|
||||
enable_extension_details: 'Erweiterung für aktuellen Benutzer aktivieren',
|
||||
disable: 'Deaktivieren',
|
||||
@@ -104,6 +130,7 @@ window.localisation.de = {
|
||||
all: 'Alle',
|
||||
only_admins_can_install:
|
||||
'(Nur Administratorkonten können Erweiterungen installieren)',
|
||||
admin_only: 'Nur für Admins',
|
||||
new_version: 'Neue Version',
|
||||
extension_depends_on: 'Hängt ab von:',
|
||||
extension_rating_soon: 'Bewertungen sind bald verfügbar',
|
||||
@@ -111,14 +138,112 @@ window.localisation.de = {
|
||||
extension_uninstall_warning:
|
||||
'Sie sind dabei, die Erweiterung für alle Benutzer zu entfernen.',
|
||||
uninstall_confirm: 'Ja, deinstallieren',
|
||||
extension_db_drop_info:
|
||||
'Alle Daten für die Erweiterung werden dauerhaft gelöscht. Es gibt keine Möglichkeit, diesen Vorgang rückgängig zu machen!',
|
||||
extension_db_drop_warning:
|
||||
'Sie sind dabei, alle Daten für die Erweiterung zu entfernen. Bitte geben Sie den Namen der Erweiterung ein, um fortzufahren:',
|
||||
extension_min_lnbits_version:
|
||||
'Diese Version erfordert mindestens die LNbits-Version',
|
||||
|
||||
payment_hash: 'Zahlungs-Hash',
|
||||
fee: 'Gebühr',
|
||||
amount: 'Menge',
|
||||
tag: 'Tag',
|
||||
unit: 'Einheit',
|
||||
description: 'Beschreibung',
|
||||
expiry: 'Ablauf',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Beleg'
|
||||
payment_proof: 'Beleg',
|
||||
update_available: 'Aktualisierung %{version} verfügbar!',
|
||||
latest_update: 'Sie sind auf der neuesten Version %{version}.',
|
||||
notifications: 'Benachrichtigungen',
|
||||
no_notifications: 'Keine Benachrichtigungen',
|
||||
notifications_disabled: 'LNbits Statusbenachrichtigungen sind deaktiviert.',
|
||||
enable_notifications: 'Aktiviere Benachrichtigungen',
|
||||
enable_notifications_desc:
|
||||
'Wenn aktiviert, werden die neuesten LNbits-Statusaktualisierungen, wie Sicherheitsvorfälle und Updates, abgerufen.',
|
||||
enable_killswitch: 'Aktivieren Sie den Notausschalter',
|
||||
enable_killswitch_desc:
|
||||
'Falls aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn LNbits ein Killswitch-Signal sendet. Nach einem Update müssen Sie dies manuell wieder aktivieren.',
|
||||
killswitch_interval: 'Intervall für den Notausschalter',
|
||||
killswitch_interval_desc:
|
||||
'Wie oft die Hintergrundaufgabe nach dem LNBits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
|
||||
enable_watchdog: 'Aktiviere Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Wenn aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn Ihr Guthaben niedriger als das LNbits-Guthaben ist. Nach einem Update müssen Sie dies manuell aktivieren.',
|
||||
watchdog_interval: 'Überwachungszeitintervall',
|
||||
watchdog_interval_desc:
|
||||
'Wie oft die Hintergrundaufgabe nach einem Abschaltsignal im Wachhund-Delta [node_balance - lnbits_balance] suchen soll (in Minuten).',
|
||||
watchdog_delta: 'Watchdog Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limit, bevor der Notausschalter die Finanzierungsquelle auf VoidWallet ändert [lnbits_balance - node_balance > delta]',
|
||||
status: 'Status',
|
||||
notification_source: 'Benachrichtigungsquelle',
|
||||
notification_source_label:
|
||||
'Quell-URL (verwenden Sie nur die offizielle LNbits-Statusquelle und Quellen, denen Sie vertrauen können)',
|
||||
more: 'mehr',
|
||||
less: 'weniger',
|
||||
releases: 'Veröffentlichungen',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Wachhund',
|
||||
server_logs: 'Serverprotokolle',
|
||||
ip_blocker: 'IP-Sperre',
|
||||
security: 'Sicherheit',
|
||||
security_tools: 'Sicherheitstools',
|
||||
block_access_hint: 'Zugriff per IP sperren',
|
||||
allow_access_hint: 'Zugriff durch IP erlauben (überschreibt blockierte IPs)',
|
||||
enter_ip: 'Geben Sie die IP ein und drücken Sie die Eingabetaste',
|
||||
rate_limiter: 'Ratenbegrenzer',
|
||||
wallet_limiter: 'Geldbeutel-Limiter',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximales tägliches Wallet-Auszahlungslimit in Sats (0 zum Deaktivieren)',
|
||||
wallet_max_ballance:
|
||||
'Maximales Guthaben der Wallet in Sats (0 zum Deaktivieren)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Mindestsekunden zwischen Transaktionen pro Wallet (0 zum Deaktivieren)',
|
||||
number_of_requests: 'Anzahl der Anfragen',
|
||||
time_unit: 'Zeiteinheit',
|
||||
minute: 'Minute',
|
||||
second: 'Sekunde',
|
||||
hour: 'Stunde',
|
||||
disable_server_log: 'Server-Log deaktivieren',
|
||||
enable_server_log: 'Serverprotokollierung aktivieren',
|
||||
coming_soon: 'Funktion demnächst verfügbar',
|
||||
session_has_expired:
|
||||
'Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.',
|
||||
instant_access_question: 'Möchten Sie sofortigen Zugang?',
|
||||
login_with_user_id: 'Mit Benutzer-ID anmelden',
|
||||
or: 'oder',
|
||||
create_new_wallet: 'Neue Geldbörse erstellen',
|
||||
login_to_account: 'Melden Sie sich bei Ihrem Konto an',
|
||||
create_account: 'Konto erstellen',
|
||||
account_settings: 'Kontoeinstellungen',
|
||||
signin_with_google: 'Mit Google anmelden',
|
||||
signin_with_github: 'Anmelden mit GitHub',
|
||||
signin_with_keycloak: 'Mit Keycloak anmelden',
|
||||
username_or_email: 'Benutzername oder E-Mail',
|
||||
password: 'Passwort',
|
||||
password_config: 'Passwortkonfiguration',
|
||||
password_repeat: 'Passwortwiederholung',
|
||||
change_password: 'Passwort ändern',
|
||||
set_password: 'Passwort festlegen',
|
||||
invalid_password: 'Das Passwort muss mindestens 8 Zeichen haben.',
|
||||
login: 'Anmelden',
|
||||
register: 'Registrieren',
|
||||
username: 'Benutzername',
|
||||
user_id: 'Benutzer-ID',
|
||||
email: 'E-Mail',
|
||||
first_name: 'Vorname',
|
||||
last_name: 'Nachname',
|
||||
picture: 'Bild',
|
||||
verify_email: 'E-Mail verifizieren mit',
|
||||
account: 'Konto',
|
||||
update_account: 'Konto aktualisieren',
|
||||
invalid_username: 'Ungültiger Benutzername',
|
||||
auth_provider: 'Anbieter für Authentifizierung',
|
||||
my_account: 'Mein Konto',
|
||||
back: 'Zurück',
|
||||
logout: 'Abmelden',
|
||||
look_and_feel: 'Aussehen und Verhalten',
|
||||
language: 'Sprache',
|
||||
color_scheme: 'Farbschema'
|
||||
}
|
||||
|
||||
+70
-15
@@ -3,13 +3,12 @@ window.localisation.en = {
|
||||
server: 'Server',
|
||||
theme: 'Theme',
|
||||
funding: 'Funding',
|
||||
|
||||
users: 'Users',
|
||||
apps: 'Apps',
|
||||
channels: 'Channels',
|
||||
transactions: 'Transactions',
|
||||
dashboard: 'Dashboard',
|
||||
manage_node: 'Manage Node',
|
||||
node: 'Node',
|
||||
total_capacity: 'Total Capacity',
|
||||
avg_channel_size: 'Avg. Channel Size',
|
||||
biggest_channel_size: 'Biggest Channel Size',
|
||||
@@ -36,7 +35,7 @@ window.localisation.en = {
|
||||
name_your_wallet: 'Name your %{name} wallet',
|
||||
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
|
||||
lnbits_description:
|
||||
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, Alby, ZBD, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||
export_to_phone: 'Export to Phone with QR Code',
|
||||
export_to_phone_desc:
|
||||
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
||||
@@ -55,9 +54,15 @@ window.localisation.en = {
|
||||
view_github: 'View on GitHub',
|
||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||
use_with_caution: 'USE WITH CAUTION - %{name} wallet is still in BETA',
|
||||
service_fee: 'Service fee: %{amount} % per transaction',
|
||||
service_fee_max: 'Service fee: %{amount} % per transaction (max %{max} sats)',
|
||||
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_docs: 'API docs',
|
||||
api_keys_api_docs: 'Node URL, API keys and API docs',
|
||||
lnbits_version: 'LNbits version',
|
||||
runs_on: 'Runs on',
|
||||
credit_hint: 'Press Enter to credit account',
|
||||
@@ -68,11 +73,10 @@ window.localisation.en = {
|
||||
create_invoice: 'Create Invoice',
|
||||
camera_tooltip: 'Use camera to scan an invoice/QR',
|
||||
export_csv: 'Export to CSV',
|
||||
transactions: 'Transactions',
|
||||
chart_tooltip: 'Show chart',
|
||||
pending: 'Pending',
|
||||
copy_invoice: 'Copy invoice',
|
||||
close: 'Close',
|
||||
withdraw_from: 'Withdraw from',
|
||||
cancel: 'Cancel',
|
||||
scan: 'Scan',
|
||||
read: 'Read',
|
||||
@@ -93,24 +97,25 @@ window.localisation.en = {
|
||||
'This is an LNURL-withdraw QR code for slurping everything from this wallet. Do not share with anyone. It is compatible with balanceCheck and balanceNotify so your wallet may keep pulling the funds continuously from here after the first withdraw.',
|
||||
i_understand: 'I understand',
|
||||
copy_wallet_url: 'Copy wallet URL',
|
||||
disclaimer_dialog:
|
||||
'Login functionality to be released in a future update, for now, make sure you bookmark this page for future access to your wallet! This service is in BETA, and we hold no responsibility for people losing access to funds.',
|
||||
disclaimer_dialog_title: 'Important!',
|
||||
disclaimer_dialog: `You *must* save your login credentials to be able to access your wallet again.If you lose them, you will lose access to your wallet and funds.
|
||||
|
||||
Find your login credentials on your account settings page.
|
||||
|
||||
This service is in BETA. LNbits holds no responsibility for loss of access to funds.`,
|
||||
no_transactions: 'No transactions made yet',
|
||||
manage_server: 'Manage Server',
|
||||
manage: 'Manage',
|
||||
extensions: 'Extensions',
|
||||
no_extensions: "You don't have any extensions installed :(",
|
||||
created: 'Created',
|
||||
|
||||
search_extensions: 'Search extensions',
|
||||
warning: 'Warning',
|
||||
manage: 'Manage',
|
||||
repository: 'Repository',
|
||||
confirm_continue: 'Are you sure you want to continue?',
|
||||
manage_extension_details: 'Install/uninstall extension',
|
||||
install: 'Install',
|
||||
uninstall: 'Uninstall',
|
||||
drop_db: 'Remove Data',
|
||||
open: 'Open',
|
||||
enable: 'Enable',
|
||||
enable_extension_details: 'Enable extension for current user',
|
||||
disable: 'Disable',
|
||||
@@ -134,12 +139,11 @@ window.localisation.en = {
|
||||
'All data for the extension will be permanently deleted. There is no way to undo this operation!',
|
||||
extension_db_drop_warning:
|
||||
'You are about to remove all data for the extension. Please type the extension name to continue:',
|
||||
|
||||
extension_min_lnbits_version: 'This release requires at least LNbits version',
|
||||
|
||||
payment_hash: 'Payment Hash',
|
||||
fee: 'Fee',
|
||||
amount: 'Amount',
|
||||
tag: 'Tag',
|
||||
unit: 'Unit',
|
||||
description: 'Description',
|
||||
expiry: 'Expiry',
|
||||
@@ -173,6 +177,7 @@ window.localisation.en = {
|
||||
notification_source_label:
|
||||
'Source URL (only use the official LNbits status source, and sources you can trust)',
|
||||
more: 'more',
|
||||
less: 'less',
|
||||
releases: 'Releases',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
@@ -184,6 +189,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',
|
||||
@@ -191,5 +202,49 @@ window.localisation.en = {
|
||||
hour: 'hour',
|
||||
disable_server_log: 'Disable Server Log',
|
||||
enable_server_log: 'Enable Server Log',
|
||||
coming_soon: 'Feature coming soon'
|
||||
coming_soon: 'Feature coming soon',
|
||||
session_has_expired: 'Your session has expired. Please login again.',
|
||||
instant_access_question: 'Want instant access?',
|
||||
login_with_user_id: 'Login with user ID',
|
||||
or: 'or',
|
||||
create_new_wallet: 'Create New Wallet',
|
||||
login_to_account: 'Login to your account',
|
||||
create_account: 'Create account',
|
||||
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',
|
||||
password_repeat: 'Password repeat',
|
||||
change_password: 'Change Password',
|
||||
set_password: 'Set Password',
|
||||
invalid_password: 'Password must have at least 8 characters',
|
||||
login: 'Login',
|
||||
register: 'Register',
|
||||
username: 'Username',
|
||||
user_id: 'User ID',
|
||||
email: 'Email',
|
||||
first_name: 'First Name',
|
||||
last_name: 'Last Name',
|
||||
picture: 'Picture',
|
||||
verify_email: 'Verify email with',
|
||||
account: 'Account',
|
||||
update_account: 'Update Account',
|
||||
invalid_username: 'Invalid Username',
|
||||
auth_provider: 'Auth Provider',
|
||||
my_account: 'My Account',
|
||||
back: 'Back',
|
||||
logout: 'Logout',
|
||||
look_and_feel: 'Look and Feel',
|
||||
language: 'Language',
|
||||
color_scheme: 'Color Scheme',
|
||||
extension_cost: 'This release requires a payment of minimum %{cost} sats.',
|
||||
extension_paid_sats: 'You have already paid %{paid_sats} sats.',
|
||||
release_details_error: 'Cannot get the release details.',
|
||||
pay_from_wallet: 'Pay from Wallet',
|
||||
show_qr: 'Show QR',
|
||||
retry_install: 'Retry Install',
|
||||
new_payment: 'Make New Payment'
|
||||
}
|
||||
|
||||
+139
-12
@@ -1,9 +1,26 @@
|
||||
window.localisation.es = {
|
||||
confirm: 'Sí',
|
||||
server: 'Servidor',
|
||||
theme: 'Tema',
|
||||
funding: 'Financiación',
|
||||
unit: 'Unidad',
|
||||
users: 'Usuarios',
|
||||
apps: 'Aplicaciones',
|
||||
channels: 'Canales',
|
||||
transactions: 'Transacciones',
|
||||
dashboard: 'Tablero de instrumentos',
|
||||
node: 'Nodo',
|
||||
total_capacity: 'Capacidad Total',
|
||||
avg_channel_size: 'Tamaño Medio del Canal',
|
||||
biggest_channel_size: 'Tamaño del Canal Más Grande',
|
||||
smallest_channel_size: 'Tamaño de canal más pequeño',
|
||||
number_of_channels: 'Número de canales',
|
||||
active_channels: 'Canales activos',
|
||||
connect_peer: 'Conectar Par',
|
||||
connect: 'Conectar',
|
||||
open_channel: 'Canal Abierto',
|
||||
open: 'Abrir',
|
||||
close_channel: 'Cerrar canal',
|
||||
close: 'Cerrar',
|
||||
restart: 'Reiniciar el servidor',
|
||||
save: 'Guardar',
|
||||
save_tooltip: 'Guardar cambios',
|
||||
@@ -19,7 +36,7 @@ window.localisation.es = {
|
||||
name_your_wallet: 'Nombre de su billetera %{name}',
|
||||
paste_invoice_label: 'Pegue la factura aquí',
|
||||
lnbits_description:
|
||||
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, Core Lightning, OpenNode, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
|
||||
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, Core Lightning, OpenNode, Alby, ZBD, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
|
||||
export_to_phone: 'Exportar a teléfono con código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contiene su URL de billetera con acceso completo. Puede escanearlo desde su teléfono para abrir su billetera allí.',
|
||||
@@ -30,26 +47,38 @@ window.localisation.es = {
|
||||
'Esta billetera completa se eliminará, los fondos son IRREVERSIBLES.',
|
||||
rename_wallet: 'Cambiar el nombre de la billetera',
|
||||
update_name: 'Actualizar nombre',
|
||||
fiat_tracking: 'Seguimiento Fiat',
|
||||
currency: 'Moneda',
|
||||
update_currency: 'Actualizar moneda',
|
||||
press_to_claim: 'Presione para reclamar Bitcoin',
|
||||
donate: 'Donar',
|
||||
view_github: 'Ver en GitHub',
|
||||
voidwallet_active: '¡VoidWallet está activo! Pagos desactivados',
|
||||
use_with_caution: 'USAR CON CUIDADO - %{name} Wallet aún está en BETA',
|
||||
service_fee: 'Tarifa de servicio: %{amount} % por transacción',
|
||||
service_fee_max:
|
||||
'Tarifa de servicio: %{amount} % por transacción (máx %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
'Comisión de servicio cobrada por el administrador del servidor LNbits por cada transacción saliente',
|
||||
toggle_darkmode: 'Cambiar modo oscuro',
|
||||
view_swagger_docs: 'Ver documentos de API de LNbits Swagger',
|
||||
api_docs: 'Documentos de API',
|
||||
payment_reactions: 'Reacciones de Pago',
|
||||
view_swagger_docs: 'Ver documentación de API de LNbits Swagger',
|
||||
api_docs: 'Documentación de API',
|
||||
api_keys_api_docs: 'URL del nodo, claves de API y documentación de API',
|
||||
lnbits_version: 'Versión de LNbits',
|
||||
runs_on: 'Corre en',
|
||||
credit_hint: 'Presione Enter para cargar la cuenta',
|
||||
credit_label: 'Cargar %{denomination}',
|
||||
paste: 'Pegar',
|
||||
paste_from_clipboard: 'Pegar desde el portapapeles',
|
||||
paste_request: 'Pegar solicitud',
|
||||
create_invoice: 'Crear factura',
|
||||
camera_tooltip: 'Utilice la cámara para escanear una factura / código QR',
|
||||
export_csv: 'Exportar a CSV',
|
||||
transactions: 'Transacciones',
|
||||
chart_tooltip: 'Mostrar gráfico',
|
||||
pending: 'Pendiente',
|
||||
copy_invoice: 'Copiar factura',
|
||||
close: 'Cerrar',
|
||||
withdraw_from: 'Retirar de',
|
||||
cancel: 'Cancelar',
|
||||
scan: 'Escanear',
|
||||
read: 'Leer',
|
||||
@@ -73,20 +102,18 @@ window.localisation.es = {
|
||||
disclaimer_dialog:
|
||||
'La funcionalidad de inicio de sesión se lanzará en una actualización futura, por ahora, asegúrese de guardar esta página como marcador para acceder a su billetera en el futuro. Este servicio está en BETA y no asumimos ninguna responsabilidad por personas que pierdan el acceso a sus fondos.',
|
||||
no_transactions: 'No hay transacciones todavía',
|
||||
manage_server: 'Administrar servidor',
|
||||
manage: 'Administrar',
|
||||
extensions: 'Extensiones',
|
||||
no_extensions: 'No tienes extensiones instaladas :(',
|
||||
created: 'Creado',
|
||||
|
||||
search_extensions: 'Extensiones de búsqueda',
|
||||
warning: 'Advertencia',
|
||||
manage: 'Administrar',
|
||||
repository: 'Repositorio',
|
||||
confirm_continue: '¿Está seguro de que desea continuar?',
|
||||
manage_extension_details: 'Instalar/desinstalar extensión',
|
||||
install: 'Instalar',
|
||||
uninstall: 'Desinstalar',
|
||||
open: 'Abrir',
|
||||
drop_db: 'Eliminar datos',
|
||||
enable: 'Habilitar',
|
||||
enable_extension_details: 'Habilitar extensión para el usuario actual',
|
||||
disable: 'Deshabilitar',
|
||||
@@ -100,6 +127,7 @@ window.localisation.es = {
|
||||
all: 'Todos',
|
||||
only_admins_can_install:
|
||||
'(Solo las cuentas de administrador pueden instalar extensiones)',
|
||||
admin_only: 'Solo administradores',
|
||||
new_version: 'Nueva Versión',
|
||||
extension_depends_on: 'Depende de:',
|
||||
extension_rating_soon: 'Calificaciones próximamente',
|
||||
@@ -107,14 +135,113 @@ window.localisation.es = {
|
||||
extension_uninstall_warning:
|
||||
'Está a punto de eliminar la extensión para todos los usuarios.',
|
||||
uninstall_confirm: 'Sí, desinstalar',
|
||||
extension_db_drop_info:
|
||||
'Todos los datos para la extensión se eliminarán permanentemente. ¡No hay manera de deshacer esta operación!',
|
||||
extension_db_drop_warning:
|
||||
'Está a punto de eliminar todos los datos para la extensión. Por favor, escriba el nombre de la extensión para continuar:',
|
||||
extension_min_lnbits_version:
|
||||
'Esta versión requiere al menos una versión de LNbits',
|
||||
|
||||
payment_hash: 'Hash de pago',
|
||||
fee: 'Cuota',
|
||||
amount: 'Cantidad',
|
||||
tag: 'Etiqueta',
|
||||
unit: 'Unidad',
|
||||
description: 'Descripción',
|
||||
expiry: 'Expiración',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Prueba de pago'
|
||||
payment_proof: 'Prueba de pago',
|
||||
update_available: '¡Actualización %{version} disponible!',
|
||||
latest_update: 'Usted está en la última versión %{version}.',
|
||||
notifications: 'Notificaciones',
|
||||
no_notifications: 'No hay notificaciones',
|
||||
notifications_disabled:
|
||||
'Las notificaciones de estado de LNbits están desactivadas.',
|
||||
enable_notifications: 'Activar notificaciones',
|
||||
enable_notifications_desc:
|
||||
'Si está activado, buscará las últimas actualizaciones del estado de LNbits, como incidentes de seguridad y actualizaciones.',
|
||||
enable_killswitch: 'Activar Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si LNbits envía una señal de parada de emergencia. Necesitará activarlo manualmente después de una actualización.',
|
||||
killswitch_interval: 'Intervalo de Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNBits desde la fuente de estado (en minutos).',
|
||||
enable_watchdog: 'Activar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si su saldo es inferior al saldo de LNbits. Tendrá que activarlo manualmente después de una actualización.',
|
||||
watchdog_interval: 'Intervalo de vigilancia',
|
||||
watchdog_interval_desc:
|
||||
'Con qué frecuencia la tarea de fondo debe verificar la señal de killswitch en el delta del watchdog [node_balance - lnbits_balance] (en minutos).',
|
||||
watchdog_delta: 'Vigilante Delta',
|
||||
watchdog_delta_desc:
|
||||
'Límite antes de que el interruptor de apagado cambie la fuente de financiamiento a VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Estado',
|
||||
notification_source: 'Fuente de notificación',
|
||||
notification_source_label:
|
||||
'URL de origen (solo use la fuente oficial de estado de LNbits y fuentes en las que confíe)',
|
||||
more: 'más',
|
||||
less: 'menos',
|
||||
releases: 'Lanzamientos',
|
||||
killswitch: 'Interruptor de apagado',
|
||||
watchdog: 'Perro guardián',
|
||||
server_logs: 'Registros del Servidor',
|
||||
ip_blocker: 'Bloqueador de IP',
|
||||
security: 'Seguridad',
|
||||
security_tools: 'Herramientas de seguridad',
|
||||
block_access_hint: 'Bloquear acceso por IP',
|
||||
allow_access_hint: 'Permitir acceso por IP (anulará las IPs bloqueadas)',
|
||||
enter_ip: 'Ingrese la IP y presione enter',
|
||||
rate_limiter: 'Limitador de tasa',
|
||||
wallet_limiter: 'Limitador de Cartera',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Límite diario de retiro de la cartera en sats (0 para deshabilitar)',
|
||||
wallet_max_ballance:
|
||||
'Saldo máximo de la billetera en sats (0 para desactivar)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Mín. segs entre transacciones por cartera (0 para desactivar)',
|
||||
number_of_requests: 'Número de solicitudes',
|
||||
time_unit: 'Unidad de tiempo',
|
||||
minute: 'minuto',
|
||||
second: 'segundo',
|
||||
hour: 'hora',
|
||||
disable_server_log: 'Desactivar registro del servidor',
|
||||
enable_server_log: 'Activar registro del servidor',
|
||||
coming_soon: 'Función próximamente disponible',
|
||||
session_has_expired:
|
||||
'Tu sesión ha expirado. Por favor, inicia sesión de nuevo.',
|
||||
instant_access_question: '¿Quieres acceso instantáneo?',
|
||||
login_with_user_id: 'Iniciar sesión con ID de usuario',
|
||||
or: 'o',
|
||||
create_new_wallet: 'Crear Nueva Cartera',
|
||||
login_to_account: 'Inicie sesión en su cuenta',
|
||||
create_account: 'Crear cuenta',
|
||||
account_settings: 'Configuración de la cuenta',
|
||||
signin_with_google: 'Inicia sesión con Google',
|
||||
signin_with_github: 'Inicia sesión con GitHub',
|
||||
signin_with_keycloak: 'Iniciar sesión con Keycloak',
|
||||
username_or_email: 'Nombre de usuario o correo electrónico',
|
||||
password: 'Contraseña',
|
||||
password_config: 'Configuración de Contraseña',
|
||||
password_repeat: 'Repetición de contraseña',
|
||||
change_password: 'Cambiar contraseña',
|
||||
set_password: 'Establecer contraseña',
|
||||
invalid_password: 'La contraseña debe tener al menos 8 caracteres.',
|
||||
login: 'Iniciar sesión',
|
||||
register: 'Registrarse',
|
||||
username: 'Nombre de usuario',
|
||||
user_id: 'Identificación de usuario',
|
||||
email: 'Correo electrónico',
|
||||
first_name: 'Nombre de pila',
|
||||
last_name: 'Apellido',
|
||||
picture: 'Imagen',
|
||||
verify_email: 'Verifique el correo electrónico con',
|
||||
account: 'Cuenta',
|
||||
update_account: 'Actualizar cuenta',
|
||||
invalid_username: 'Nombre de usuario inválido',
|
||||
auth_provider: 'Proveedor de Autenticación',
|
||||
my_account: 'Mi cuenta',
|
||||
back: 'Atrás',
|
||||
logout: 'Cerrar sesión',
|
||||
look_and_feel: 'Apariencia',
|
||||
language: 'Idioma',
|
||||
color_scheme: 'Esquema de colores'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
window.localisation.fi = {
|
||||
confirm: 'Kyllä',
|
||||
server: 'Palvelin',
|
||||
theme: 'Teema',
|
||||
funding: 'Rahoitus',
|
||||
users: 'Käyttäjät',
|
||||
apps: 'Sovellukset',
|
||||
channels: 'Kanavat',
|
||||
transactions: 'Tapahtumat',
|
||||
dashboard: 'Ohjauspaneeli',
|
||||
node: 'Solmu',
|
||||
total_capacity: 'Kokonaiskapasiteetti',
|
||||
avg_channel_size: 'Keskimääräisen kanavan kapasiteetti',
|
||||
biggest_channel_size: 'Suurimman kanavan kapasiteetti',
|
||||
smallest_channel_size: 'Pienimmän kanavan kapasiteetti',
|
||||
number_of_channels: 'Kanavien lukumäärä',
|
||||
active_channels: 'Aktiivisia kanavia',
|
||||
connect_peer: 'Yhdistä naapuriin',
|
||||
connect: 'Yhdistä',
|
||||
open_channel: 'Avaa kanava',
|
||||
open: 'Avaa',
|
||||
close_channel: 'Sulje kanava',
|
||||
close: 'Sulje',
|
||||
restart: 'Palvelimen uudelleen käynnistys',
|
||||
save: 'Tallenna',
|
||||
save_tooltip: 'Tallenna muutokset',
|
||||
topup: 'Topup',
|
||||
topup_wallet: 'Lisää varoja lompakkoon',
|
||||
topup_hint: 'Lisää varoja lompakkoon sen ID:n perusteella',
|
||||
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
|
||||
add_funds_tooltip: 'Lisää varoja lompakkoon',
|
||||
reset_defaults: 'Peruuta muutokset',
|
||||
reset_defaults_tooltip:
|
||||
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
|
||||
download_backup: 'Lataa tietokannan varmuuskopio',
|
||||
name_your_wallet: 'Anna %{name}-lompakollesi nimi',
|
||||
paste_invoice_label:
|
||||
'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *',
|
||||
lnbits_description:
|
||||
'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. ',
|
||||
wallets: 'Lompakot',
|
||||
add_wallet: 'Lisää lompakko',
|
||||
delete_wallet: 'Poista lompakko',
|
||||
delete_wallet_desc:
|
||||
'Lompakko poistetaan pysyvästi. Siirrä lompakosta varat ennalta muualle, sillä tämä toiminto on PERUUTTAMATON!',
|
||||
rename_wallet: 'Nimeä lompakko uudelleen',
|
||||
update_name: 'Tallenna',
|
||||
fiat_tracking: 'Käytettävä valuutta',
|
||||
currency: 'Valuutta',
|
||||
update_currency: 'Tallenna',
|
||||
press_to_claim: 'Lunasta varat painamalla tästä',
|
||||
donate: 'Lahjoita',
|
||||
view_github: 'Näytä GitHub:ssa',
|
||||
voidwallet_active:
|
||||
'Maksutapahtumat ovat poissa käytöstä, koska VoidWallet on aktiivinen!',
|
||||
use_with_caution:
|
||||
'KÄYTÄ VAROEN - BETA-ohjelmisto on käytössä palvelussa: %{name}',
|
||||
service_fee: 'Palvelumaksu: %{amount} % tapahtumasta',
|
||||
service_fee_max:
|
||||
'Palvelumaksu: %{amount} % tapahtumasta (enintään %{max} sat)',
|
||||
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: 'Solmun URL, API-avaimet ja -dokumentaatio',
|
||||
lnbits_version: 'LNbits versio',
|
||||
runs_on: 'Mukana menossa',
|
||||
credit_hint: 'Hyväksy painamalla Enter',
|
||||
credit_label: 'Lisää tilille varoja %{denomination}',
|
||||
paste: 'Liitä',
|
||||
paste_from_clipboard: 'Liitä leikepöydältä',
|
||||
paste_request: 'Liitä pyyntö',
|
||||
create_invoice: 'Laskuta',
|
||||
camera_tooltip: 'Kuvaa lasku tai QR-koodi',
|
||||
export_csv: 'Vie CSV-tiedostoon',
|
||||
chart_tooltip: 'Näytä kaaviokuva',
|
||||
pending: 'Odottaa',
|
||||
copy_invoice: 'Kopioi lasku',
|
||||
withdraw_from: 'Nosta kohteesta',
|
||||
cancel: 'Peruuta',
|
||||
scan: 'Scannaa',
|
||||
read: 'Lue',
|
||||
pay: 'Maksa',
|
||||
memo: 'Kuvaus',
|
||||
date: 'Päiväys',
|
||||
processing_payment: 'Maksua käsitellään...',
|
||||
not_enough_funds: 'Varat eivät riitä!',
|
||||
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
|
||||
invoice_waiting: 'Lasku osottaa maksamista',
|
||||
payment_received: 'Maksu vastaanotettu',
|
||||
payment_sent: 'Maksu lähetetty',
|
||||
receive: 'vastaanota',
|
||||
send: 'lähetä',
|
||||
outgoing_payment_pending: 'Lähtevä maksu odottaa',
|
||||
drain_funds: 'Tyhjennä varat',
|
||||
drain_funds_desc:
|
||||
'Tämä LNURL-withdraw -tyyppinen QR-koodi on tarkoitettu kaikkien varojen imurointiin lompakosta. ÄLÄ JAA SITÄ KENELLEKÄÄN! Se on balanceCheck- ja balanceNotify-toimintojen kanssa yhteensopiva, joten sitä voi käyttää lompakon tyhjentämiseen ensimmäisen käytön jälleen jatkuvasti.',
|
||||
i_understand: 'Vakuutan ymmärtäväni',
|
||||
copy_wallet_url: 'Kopioi lompakon URL',
|
||||
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: 'Hallinnointi',
|
||||
extensions: 'Laajennukset',
|
||||
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
||||
created: 'Luotu',
|
||||
search_extensions: 'Etsi laajennuksia',
|
||||
warning: 'Varoitus',
|
||||
repository: 'Laajennuksien lähde',
|
||||
confirm_continue: 'Haluatko varmasti jatkaa?',
|
||||
manage_extension_details: 'Asenna/Poista laajennus',
|
||||
install: 'Asenna',
|
||||
uninstall: 'Poista',
|
||||
drop_db: 'Poista tiedot',
|
||||
enable: 'Ota käyttöön',
|
||||
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
|
||||
disable: 'Poista käytöstä',
|
||||
installed: 'Asennettu',
|
||||
activated: 'Käytössä',
|
||||
deactivated: 'Poissa käytöstä',
|
||||
release_notes: 'Julkaisutiedot',
|
||||
activate_extension_details: 'Aseta/Poista laajennus käyttäjien saatavilta',
|
||||
featured: 'Esittelyssä',
|
||||
all: 'Kaikki',
|
||||
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
|
||||
admin_only: 'Pääkäyttäjille',
|
||||
new_version: 'Uusi versio',
|
||||
extension_depends_on: 'Edellyttää:',
|
||||
extension_rating_soon: 'Arvostelut on tulossa pian',
|
||||
extension_installed_version: 'Nykyinen versio',
|
||||
extension_uninstall_warning:
|
||||
'Olet poistamassa laajennuksen kaikilta käyttäjiltä.',
|
||||
uninstall_confirm: 'Kyllä, poista asennus',
|
||||
extension_db_drop_info:
|
||||
'Kaikki laajennuksen tallettama tieto poistetaan pysyvästi. Poistoa ei voi jälkikäteen peruuttaa!',
|
||||
extension_db_drop_warning:
|
||||
'Olet tuhoamassa laajennuksen tallettamat tiedot. Vahvista poisto kirjoittamalla viivalle seuraavassa näkyvä laajennuksen nimi:',
|
||||
extension_min_lnbits_version: 'Tämä julkaisu vaatii vähintään LNbits-version',
|
||||
payment_hash: 'Maksun tiiviste',
|
||||
fee: 'Kulu',
|
||||
amount: 'Määrä',
|
||||
tag: 'Tunniste',
|
||||
unit: 'Yksikkö',
|
||||
description: 'Kuvaus',
|
||||
expiry: 'Vanheneminen',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Maksun varmenne',
|
||||
update_available: 'Saatavilla on päivitys versioon %{version}!',
|
||||
latest_update:
|
||||
'Käytössä oleva versio %{version}, on viimeisin saatavilla oleva.',
|
||||
notifications: 'Tiedotteet',
|
||||
no_notifications: 'Ei tiedotteita',
|
||||
notifications_disabled: 'LNbits-tilatiedotteet on poistettu käytöstä.',
|
||||
enable_notifications: 'Ota tiedotteet käyttöön',
|
||||
enable_notifications_desc:
|
||||
'Tämän ollessa valittuna, noudetaan LNbits-tilatiedotteet. Niitä ovat esimerkiksi turvallisuuteen liittyvät tapahtumatiedotteet ja tiedot tämän ohjelmiston päivityksistä.',
|
||||
enable_killswitch: 'Ota Killswitch käyttöön',
|
||||
enable_killswitch_desc:
|
||||
'Jos LNbits antaa killswitch-komennon, niin rahoituslähteeksi valitaan automaattisesti heti VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
|
||||
killswitch_interval: 'Killswitch-aikaväli',
|
||||
killswitch_interval_desc:
|
||||
'Tällä määritetään kuinka usein taustatoiminto tarkistaa killswitch-signaalin tilatiedotteiden lähteestä. Hakujen väli ilmoitetaan minuutteina.',
|
||||
enable_watchdog: 'Ota Watchdog käyttöön',
|
||||
enable_watchdog_desc:
|
||||
'Tämän ollessa käytössä, ja solmun varojen laskiessa alle LNbits-varojen määrän, otetaan automaattisesti käyttöön VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
|
||||
watchdog_interval: 'Watchdog-aikaväli',
|
||||
watchdog_interval_desc:
|
||||
'Tällä määritetään kuinka usein taustatoiminto tarkistaa varojen Delta-muutokset [node_balance - lnbits_balance] killswitch-signaalille. Hakujen väli ilmoitetaan minuutteina.',
|
||||
watchdog_delta: 'Watchdog Delta',
|
||||
watchdog_delta_desc:
|
||||
'Saldomuutoksen raja-arvo jolloin killswitch-muuttaa rahoituslähteeksi VoidWallet:in [lnbits_balance - node_balance > delta]',
|
||||
status: 'Tilanne',
|
||||
notification_source: 'Tiedotteiden lähde',
|
||||
notification_source_label:
|
||||
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
||||
more: 'enemmän',
|
||||
less: 'vähemmän',
|
||||
releases: 'Julkaisut',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Palvelimen lokit',
|
||||
ip_blocker: 'IP-suodatin',
|
||||
security: 'Turvallisuus',
|
||||
security_tools: 'Turvallisuus työkalut',
|
||||
block_access_hint: 'Estä pääsy IP-osoitteen perusteella',
|
||||
allow_access_hint: 'Salli pääsy IP-osoitteen perusteella (ohittaa estot)',
|
||||
enter_ip: 'Anna IP ja paina +',
|
||||
rate_limiter: 'Toiston rajoitin',
|
||||
wallet_limiter: 'Lompakon Rajoitin',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maksimi päivittäinen lompakon nosto sateissa (0 poistaa käytöstä)',
|
||||
wallet_max_ballance: 'Lompakon maksimisaldo satosheina (0 poistaa käytöstä)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min sekuntia transaktioiden välillä lompakkoa kohden (0 poistaa käytöstä)',
|
||||
number_of_requests: 'Pyyntöjen lukumäärä',
|
||||
time_unit: 'aikayksikkö',
|
||||
minute: 'minuutti',
|
||||
second: 'sekunti',
|
||||
hour: 'tunti',
|
||||
disable_server_log: 'Poista palvelimen loki käytöstä',
|
||||
enable_server_log: 'Ota palvelimen loki käyttöön',
|
||||
coming_soon: 'Ominaisuus on tulossa pian',
|
||||
session_has_expired: 'Käyttämätön sessio on vanhentunut. Kirjaudu uudelleen.',
|
||||
instant_access_question: 'Kirjaudu aikaisemmin luodulla tiedolla',
|
||||
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
||||
or: 'tai',
|
||||
create_new_wallet: 'Avaa uusi lompakko',
|
||||
login_to_account: 'Kirjaudu käyttäjänimellä',
|
||||
create_account: 'Luo tili',
|
||||
account_settings: 'Tilin asetukset',
|
||||
signin_with_google: 'Kirjaudu Google-tunnuksella',
|
||||
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
|
||||
signin_with_keycloak: 'Kirjaudu Keycloak-tunnuksella',
|
||||
username_or_email: 'Käyttäjänimi tai sähköposti',
|
||||
password: 'Anna uusi salasana',
|
||||
password_config: 'Salasanan määritys',
|
||||
password_repeat: 'Toista uusi salasana',
|
||||
change_password: 'Vaihda salasana',
|
||||
set_password: 'Aseta salasana',
|
||||
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
||||
login: 'Kirjaudu',
|
||||
register: 'Rekisteröidy',
|
||||
username: 'Käyttäjänimi',
|
||||
user_id: 'Käyttäjä ID',
|
||||
email: 'Sähköposti',
|
||||
first_name: 'Etunimi',
|
||||
last_name: 'Sukunimi',
|
||||
picture: 'Kuva',
|
||||
verify_email: 'Vahvista sähköposti',
|
||||
account: 'Tili',
|
||||
update_account: 'Päivitä tiliä',
|
||||
invalid_username: 'Virheellinen käyttäjänimi',
|
||||
auth_provider: 'Tunnistamisen toimittaja',
|
||||
my_account: 'Tilini',
|
||||
back: 'Takaisin',
|
||||
logout: 'Poistu',
|
||||
look_and_feel: 'Kieli ja värit',
|
||||
language: 'Kieli',
|
||||
color_scheme: 'Väriteema'
|
||||
}
|
||||
+166
-8
@@ -1,8 +1,26 @@
|
||||
window.localisation.fr = {
|
||||
confirm: 'Oui',
|
||||
server: 'Serveur',
|
||||
theme: 'Thème',
|
||||
funding: 'Financement',
|
||||
users: 'Utilisateurs',
|
||||
apps: 'Applications',
|
||||
channels: 'Canaux',
|
||||
transactions: 'Transactions',
|
||||
dashboard: 'Tableau de bord',
|
||||
node: 'Noeud',
|
||||
total_capacity: 'Capacité totale',
|
||||
avg_channel_size: 'Taille moyenne du canal',
|
||||
biggest_channel_size: 'Taille de canal maximale',
|
||||
smallest_channel_size: 'Taille de canal la plus petite',
|
||||
number_of_channels: 'Nombre de canaux',
|
||||
active_channels: 'Canaux actifs',
|
||||
connect_peer: 'Connecter un pair',
|
||||
connect: 'Connecter',
|
||||
open_channel: 'Ouvrir le canal',
|
||||
open: 'Ouvrir',
|
||||
close_channel: 'Fermer le canal',
|
||||
close: 'Fermer',
|
||||
restart: 'Redémarrer le serveur',
|
||||
save: 'Enregistrer',
|
||||
save_tooltip: 'Enregistrer vos modifications',
|
||||
@@ -21,7 +39,7 @@ window.localisation.fr = {
|
||||
paste_invoice_label:
|
||||
'Coller une facture, une demande de paiement ou un code lnurl *',
|
||||
lnbits_description:
|
||||
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, Core Lightning, OpenNode, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
|
||||
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, Core Lightning, OpenNode, Alby, ZBD, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
|
||||
export_to_phone: 'Exporter vers le téléphone avec un code QR',
|
||||
export_to_phone_desc:
|
||||
"Ce code QR contient l'URL de votre portefeuille avec un accès complet. Vous pouvez le scanner depuis votre téléphone pour ouvrir votre portefeuille depuis là-bas.",
|
||||
@@ -32,28 +50,39 @@ window.localisation.fr = {
|
||||
'Ce portefeuille entier sera supprimé et les fonds seront IRRECUPERABLES.',
|
||||
rename_wallet: 'Renommer le portefeuille',
|
||||
update_name: 'Mettre à jour le nom',
|
||||
fiat_tracking: 'Suivi Fiat',
|
||||
currency: 'Devise',
|
||||
update_currency: 'Mettre à jour la devise',
|
||||
press_to_claim: 'Appuyez pour demander du Bitcoin',
|
||||
donate: 'Donner',
|
||||
view_github: 'Voir sur GitHub',
|
||||
voidwallet_active: 'VoidWallet est actif! Paiements désactivés',
|
||||
use_with_caution:
|
||||
'UTILISER AVEC PRUDENCE - Le portefeuille %{name} est toujours en version BETA',
|
||||
service_fee: 'Frais de service : %{amount} % par transaction',
|
||||
service_fee_max:
|
||||
'Frais de service : %{amount} % par transaction (max %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
"Frais de service facturés par l'administrateur du serveur LNbits pour chaque transaction sortante",
|
||||
toggle_darkmode: 'Basculer le mode sombre',
|
||||
view_swagger_docs: "Voir les documents de l'API Swagger de LNbits",
|
||||
api_docs: "Documents de l'API",
|
||||
payment_reactions: 'Réactions de paiement',
|
||||
view_swagger_docs: "Voir les documentation de l'API Swagger de LNbits",
|
||||
api_docs: "Documentation de l'API",
|
||||
api_keys_api_docs: 'URL du nœud, clés API et documentation API',
|
||||
lnbits_version: 'Version de LNbits',
|
||||
runs_on: 'Fonctionne sur',
|
||||
credit_hint: 'Appuyez sur Entrée pour créditer le compte',
|
||||
credit_label: '%{denomination} à créditer',
|
||||
paste: 'Coller',
|
||||
paste_from_clipboard: 'Coller depuis le presse-papiers',
|
||||
paste_request: 'Coller la requête',
|
||||
create_invoice: 'Créer une facture',
|
||||
camera_tooltip: 'Utiliser la caméra pour scanner une facture / un code QR',
|
||||
export_csv: 'Exporter vers CSV',
|
||||
transactions: 'Transactions',
|
||||
chart_tooltip: 'Afficher le graphique',
|
||||
pending: 'En attente',
|
||||
copy_invoice: 'Copier la facture',
|
||||
close: 'Fermer',
|
||||
withdraw_from: 'Retirer de',
|
||||
cancel: 'Annuler',
|
||||
scan: 'Scanner',
|
||||
read: 'Lire',
|
||||
@@ -77,17 +106,146 @@ window.localisation.fr = {
|
||||
disclaimer_dialog:
|
||||
"La fonctionnalité de connexion sera publiée dans une future mise à jour, pour l'instant, assurez-vous de mettre cette page en favori pour accéder à votre portefeuille ultérieurement ! Ce service est en BETA, et nous ne sommes pas responsables des personnes qui perdent l'accès à leurs fonds.",
|
||||
no_transactions: 'Aucune transaction effectuée pour le moment',
|
||||
manage_extensions: 'Gérer les extensions',
|
||||
manage_server: 'Gérer le serveur',
|
||||
manage: 'Gérer',
|
||||
extensions: 'Extensions',
|
||||
no_extensions: "Vous n'avez installé aucune extension :(",
|
||||
created: 'Créé',
|
||||
search_extensions: 'Rechercher des extensions',
|
||||
warning: 'Avertissement',
|
||||
repository: 'Référentiel',
|
||||
confirm_continue: 'Êtes-vous sûr de vouloir continuer ?',
|
||||
manage_extension_details: "Installer/désinstaller l'extension",
|
||||
install: 'Installer',
|
||||
uninstall: 'Désinstaller',
|
||||
drop_db: 'Supprimer les données',
|
||||
enable: 'Activer',
|
||||
enable_extension_details: "Activer l'extension pour l'utilisateur actuel",
|
||||
disable: 'Désactiver',
|
||||
installed: 'Installé',
|
||||
activated: 'Activé',
|
||||
deactivated: 'Désactivé',
|
||||
release_notes: 'Notes de version',
|
||||
activate_extension_details:
|
||||
"Rendre l'extension disponible/indisponible pour les utilisateurs",
|
||||
featured: 'Mis en avant',
|
||||
all: 'Tout',
|
||||
only_admins_can_install:
|
||||
'Seuls les comptes administrateurs peuvent installer des extensions',
|
||||
admin_only: 'Réservé aux administrateurs',
|
||||
new_version: 'Nouvelle version',
|
||||
extension_depends_on: 'Dépend de :',
|
||||
extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
|
||||
extension_installed_version: 'Version installée',
|
||||
extension_uninstall_warning:
|
||||
"Vous êtes sur le point de supprimer l'extension pour tous les utilisateurs.",
|
||||
uninstall_confirm: 'Oui, Désinstaller',
|
||||
extension_db_drop_info:
|
||||
"Toutes les données pour l'extension seront supprimées de manière permanente. Il n'est pas possible d'annuler cette opération !",
|
||||
extension_db_drop_warning:
|
||||
"Vous êtes sur le point de supprimer toutes les données de l'extension. Veuillez taper le nom de l'extension pour continuer :",
|
||||
extension_min_lnbits_version:
|
||||
'Cette version nécessite au moins LNbits version',
|
||||
payment_hash: 'Hash de paiement',
|
||||
fee: 'Frais',
|
||||
amount: 'Montant',
|
||||
tag: 'Étiqueter',
|
||||
unit: 'Unité',
|
||||
description: 'Description',
|
||||
expiry: 'Expiration',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Preuve de paiement'
|
||||
payment_proof: 'Preuve de paiement',
|
||||
update_available: 'Mise à jour %{version} disponible !',
|
||||
latest_update: 'Vous êtes sur la dernière version %{version}.',
|
||||
notifications: 'Notifications',
|
||||
no_notifications: 'Aucune notification',
|
||||
notifications_disabled:
|
||||
'Les notifications de statut LNbits sont désactivées.',
|
||||
enable_notifications: 'Activer les notifications',
|
||||
enable_notifications_desc:
|
||||
'Si activé, il récupérera les dernières mises à jour du statut LNbits, telles que les incidents de sécurité et les mises à jour.',
|
||||
enable_killswitch: 'Activer le Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Si activé, il changera automatiquement votre source de financement en VoidWallet si LNbits envoie un signal de coupure. Vous devrez activer manuellement après une mise à jour.',
|
||||
killswitch_interval: 'Intervalle du Killswitch',
|
||||
killswitch_interval_desc:
|
||||
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNBits provenant de la source de statut (en minutes).",
|
||||
enable_watchdog: 'Activer le Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Si elle est activée, elle changera automatiquement votre source de financement en VoidWallet si votre solde est inférieur au solde LNbits. Vous devrez activer manuellement après une mise à jour.',
|
||||
watchdog_interval: 'Intervalle du gardien',
|
||||
watchdog_interval_desc:
|
||||
"À quelle fréquence la tâche en arrière-plan doit-elle vérifier la présence d'un signal d'arrêt d'urgence dans le delta du gardien [node_balance - lnbits_balance] (en minutes).",
|
||||
watchdog_delta: 'Chien de garde Delta',
|
||||
watchdog_delta_desc:
|
||||
"Limite avant que l'interrupteur d'arrêt ne change la source de financement pour VoidWallet [lnbits_balance - node_balance > delta]",
|
||||
status: 'Statut',
|
||||
notification_source: 'Source de notification',
|
||||
notification_source_label:
|
||||
'URL source (utilisez uniquement la source officielle de statut LNbits et des sources de confiance)',
|
||||
more: 'plus',
|
||||
less: 'moins',
|
||||
releases: 'Versions',
|
||||
killswitch: "Interrupteur d'arrêt",
|
||||
watchdog: 'Chien de garde',
|
||||
server_logs: 'Journaux du serveur',
|
||||
ip_blocker: "Bloqueur d'IP",
|
||||
security: 'Sécurité',
|
||||
security_tools: 'Outils de sécurité',
|
||||
block_access_hint: "Bloquer l'accès par IP",
|
||||
allow_access_hint:
|
||||
"Autoriser l'accès par IP (cela passera outre les IP bloquées)",
|
||||
enter_ip: "Entrez l'adresse IP et appuyez sur Entrée",
|
||||
rate_limiter: 'Limiteur de débit',
|
||||
wallet_limiter: 'Limiteur de portefeuille',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Retrait quotidien maximum du portefeuille en sats (0 pour désactiver)',
|
||||
wallet_max_ballance:
|
||||
'Solde maximum du portefeuille en sats (0 pour désactiver)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minutes et secondes entre les transactions par portefeuille (0 pour désactiver)',
|
||||
number_of_requests: 'Nombre de requêtes',
|
||||
time_unit: 'Unité de temps',
|
||||
minute: 'minute',
|
||||
second: 'seconde',
|
||||
hour: 'heure',
|
||||
disable_server_log: 'Désactiver le journal du serveur',
|
||||
enable_server_log: 'Activer le journal du serveur',
|
||||
coming_soon: 'Fonctionnalité à venir bientôt',
|
||||
session_has_expired: 'Votre session a expiré. Veuillez vous reconnecter.',
|
||||
instant_access_question: 'Voulez-vous un accès instantané ?',
|
||||
login_with_user_id: "Connexion avec l'identifiant utilisateur",
|
||||
or: 'ou',
|
||||
create_new_wallet: 'Créer un nouveau portefeuille',
|
||||
login_to_account: 'Connectez-vous à votre compte',
|
||||
create_account: 'Créer un compte',
|
||||
account_settings: 'Paramètres du compte',
|
||||
signin_with_google: 'Connectez-vous avec Google',
|
||||
signin_with_github: 'Connectez-vous avec GitHub',
|
||||
signin_with_keycloak: 'Connectez-vous avec Keycloak',
|
||||
username_or_email: "Nom d'utilisateur ou e-mail",
|
||||
password: 'Mot de passe',
|
||||
password_config: 'Configuration du mot de passe',
|
||||
password_repeat: 'Répétition du mot de passe',
|
||||
change_password: 'Changer le mot de passe',
|
||||
set_password: 'Définir le mot de passe',
|
||||
invalid_password: 'Le mot de passe doit comporter au moins 8 caractères',
|
||||
login: 'Connexion',
|
||||
register: 'Inscrire',
|
||||
username: "Nom d'utilisateur",
|
||||
user_id: 'Identifiant utilisateur',
|
||||
email: 'E-mail',
|
||||
first_name: 'Prénom',
|
||||
last_name: 'Nom de famille',
|
||||
picture: 'Image',
|
||||
verify_email: "Vérifiez l'e-mail avec",
|
||||
account: 'Compte',
|
||||
update_account: 'Mettre à jour le compte',
|
||||
invalid_username: "Nom d'utilisateur invalide",
|
||||
auth_provider: "Fournisseur d'authentification",
|
||||
my_account: 'Mon compte',
|
||||
back: 'Retour',
|
||||
logout: 'Déconnexion',
|
||||
look_and_feel: 'Apparence',
|
||||
language: 'Langue',
|
||||
color_scheme: 'Schéma de couleurs'
|
||||
}
|
||||
|
||||
+169
-11
@@ -1,8 +1,26 @@
|
||||
window.localisation.it = {
|
||||
confirm: 'Sì',
|
||||
server: 'Server',
|
||||
theme: 'Tema',
|
||||
funding: 'Funding',
|
||||
users: 'Utenti',
|
||||
apps: 'Applicazioni',
|
||||
channels: 'Canali',
|
||||
transactions: 'Transazioni',
|
||||
dashboard: 'Pannello di controllo',
|
||||
node: 'Interruttore',
|
||||
total_capacity: 'Capacità Totale',
|
||||
avg_channel_size: 'Dimensione media del canale',
|
||||
biggest_channel_size: 'Dimensione del canale più grande',
|
||||
smallest_channel_size: 'Dimensione Più Piccola del Canale',
|
||||
number_of_channels: 'Numero di Canali',
|
||||
active_channels: 'Canali Attivi',
|
||||
connect_peer: 'Connetti Peer',
|
||||
connect: 'Connetti',
|
||||
open_channel: 'Canale aperto',
|
||||
open: 'Apri',
|
||||
close_channel: 'Chiudi Canale',
|
||||
close: 'Chiudi',
|
||||
restart: 'Riavvia il server',
|
||||
save: 'Salva',
|
||||
save_tooltip: 'Salva le modifiche',
|
||||
@@ -19,7 +37,7 @@ window.localisation.it = {
|
||||
paste_invoice_label:
|
||||
'Incolla una fattura, una richiesta di pagamento o un codice lnurl *',
|
||||
lnbits_description:
|
||||
"Leggero e facile da configurare, LNbits può funzionare su qualsiasi fonte di finanziamento lightning-network, attualmente supporta LND, Core Lightning, OpenNode, LNPay e persino LNbits stesso! Potete gestire LNbits per conto vostro o offrire facilmente una soluzione di custodia per altri. Ogni portafoglio ha le proprie chiavi API e non c'è limite al numero di portafogli che si possono creare. La possibilità di suddividere i fondi rende LNbits uno strumento utile per la gestione del denaro e come strumento di sviluppo. Le estensioni aggiungono ulteriori funzionalità a LNbits, consentendo di sperimentare una serie di tecnologie all'avanguardia sulla rete Lightning. Abbiamo reso lo sviluppo delle estensioni il più semplice possibile e, in quanto progetto libero e open-source, incoraggiamo le persone a sviluppare e inviare le proprie",
|
||||
"Leggero e facile da configurare, LNbits può funzionare su qualsiasi fonte di finanziamento lightning-network, attualmente supporta LND, Core Lightning, OpenNode, Alby, ZBD, LNPay e persino LNbits stesso! Potete gestire LNbits per conto vostro o offrire facilmente una soluzione di custodia per altri. Ogni portafoglio ha le proprie chiavi API e non c'è limite al numero di portafogli che si possono creare. La possibilità di suddividere i fondi rende LNbits uno strumento utile per la gestione del denaro e come strumento di sviluppo. Le estensioni aggiungono ulteriori funzionalità a LNbits, consentendo di sperimentare una serie di tecnologie all'avanguardia sulla rete Lightning. Abbiamo reso lo sviluppo delle estensioni il più semplice possibile e, in quanto progetto libero e open-source, incoraggiamo le persone a sviluppare e inviare le proprie",
|
||||
export_to_phone: 'Esportazione su telefono con codice QR',
|
||||
export_to_phone_desc:
|
||||
"Questo codice QR contiene l'URL del portafoglio con accesso da amministratore. È possibile scansionarlo dal telefono per aprire il portafoglio da lì.",
|
||||
@@ -30,33 +48,44 @@ window.localisation.it = {
|
||||
"L'intero portafoglio sarà cancellato, i fondi saranno irrecuperabili",
|
||||
rename_wallet: 'Rinomina il portafoglio',
|
||||
update_name: 'Aggiorna il nome',
|
||||
fiat_tracking: 'Tracciamento Fiat',
|
||||
currency: 'Valuta',
|
||||
update_currency: 'Aggiorna valuta',
|
||||
press_to_claim: 'Premi per richiedere bitcoin',
|
||||
donate: 'Donazioni',
|
||||
view_github: 'Visualizza su GitHub',
|
||||
voidwallet_active: 'VoidWallet è attivo! Pagamenti disabilitati',
|
||||
use_with_caution: 'USARE CON CAUTELA - %{nome} portafoglio è ancora in BETA',
|
||||
use_with_caution: 'USARE CON CAUTELA - %{name} portafoglio è ancora in BETA',
|
||||
service_fee: 'Commissione di servizio: %{amount} % per transazione',
|
||||
service_fee_max:
|
||||
'Commissione di servizio: %{amount} % per transazione (max %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
"Commissione di servizio addebitata dall'amministratore del server LNbits per ogni transazione in uscita",
|
||||
toggle_darkmode: 'Attiva la modalità notturna',
|
||||
view_swagger_docs: "Visualizza i documenti dell'API Swagger di LNbits",
|
||||
api_docs: 'Documenti API',
|
||||
payment_reactions: 'Reazioni al Pagamento',
|
||||
view_swagger_docs: "Visualizza i documentazione dell'API Swagger di LNbits",
|
||||
api_docs: "Documentazione dell'API",
|
||||
api_keys_api_docs: 'URL del nodo, chiavi API e documentazione API',
|
||||
lnbits_version: 'Versione di LNbits',
|
||||
runs_on: 'Esegue su',
|
||||
credit_hint: 'Premere Invio per accreditare i fondi',
|
||||
credit_label: '%{denomination} da accreditare',
|
||||
paste: 'Incolla',
|
||||
paste_from_clipboard: 'Incolla dagli appunti',
|
||||
paste_request: 'Richiesta di pagamento',
|
||||
create_invoice: 'Crea fattura',
|
||||
camera_tooltip: 'Usa la fotocamera per scansionare la fattura/QR',
|
||||
export_csv: 'Esporta CSV',
|
||||
transactions: 'Transazioni',
|
||||
chart_tooltip: 'Mostra grafico',
|
||||
pending: 'In attesa',
|
||||
copy_invoice: 'Copia fattura',
|
||||
close: 'Chiudi',
|
||||
withdraw_from: 'Prelevare da',
|
||||
cancel: 'Annulla',
|
||||
scan: 'Scansiona',
|
||||
read: 'Leggi',
|
||||
pay: 'Paga',
|
||||
memo: 'Memo',
|
||||
data: 'Dati',
|
||||
date: 'Dati',
|
||||
processing_payment: 'Elaborazione pagamento...',
|
||||
not_enough_funds: 'Non ci sono abbastanza fondi!',
|
||||
search_by_tag_memo_amount: 'Cerca per tag, memo, importo...',
|
||||
@@ -74,17 +103,146 @@ window.localisation.it = {
|
||||
disclaimer_dialog:
|
||||
"La funzionalità di login sarà rilasciata in un futuro aggiornamento; per ora, assicuratevi di salvare tra i preferiti questa pagina per accedere nuovamente in futuro a questo portafoglio! Questo servizio è in fase BETA e non ci assumiamo alcuna responsabilità per la perdita all'accesso dei fondi",
|
||||
no_transactions: 'Nessuna transazione effettuata',
|
||||
manage_extensions: 'Gestisci le estensioni',
|
||||
manage_server: 'Gestisci server',
|
||||
estensioni: 'Estensioni',
|
||||
manage: 'Gestisci',
|
||||
extensions: 'Estensioni',
|
||||
no_extensions: 'Non ci sono estensioni installate :(',
|
||||
created: 'Creato',
|
||||
search_extensions: 'Estensioni di ricerca',
|
||||
warning: 'Attenzione',
|
||||
repository: 'Deposito',
|
||||
confirm_continue: 'Sei sicuro di voler continuare?',
|
||||
manage_extension_details: 'Installa/disinstalla estensione',
|
||||
install: 'Installare',
|
||||
uninstall: 'Disinstalla',
|
||||
drop_db: 'Rimuovi Dati',
|
||||
enable: 'Abilita',
|
||||
enable_extension_details: "Attiva l'estensione per l'utente corrente",
|
||||
disable: 'Disabilita',
|
||||
installed: 'Installato',
|
||||
activated: 'Attivato',
|
||||
deactivated: 'Disattivato',
|
||||
release_notes: 'Note di Rilascio',
|
||||
activate_extension_details:
|
||||
"Rendi l'estensione disponibile/non disponibile per gli utenti",
|
||||
featured: 'In primo piano',
|
||||
all: 'Tutto',
|
||||
only_admins_can_install:
|
||||
'Solo gli account amministratore possono installare estensioni.',
|
||||
admin_only: 'Solo amministratore',
|
||||
new_version: 'Nuova Versione',
|
||||
extension_depends_on: 'Dipende da:',
|
||||
extension_rating_soon: 'Valutazioni in arrivo',
|
||||
extension_installed_version: 'Versione installata',
|
||||
extension_uninstall_warning:
|
||||
"Stai per rimuovere l'estensione per tutti gli utenti.",
|
||||
uninstall_confirm: 'Sì, Disinstalla',
|
||||
extension_db_drop_info:
|
||||
"Tutti i dati relativi all'estensione saranno cancellati permanentemente. Non c'è modo di annullare questa operazione!",
|
||||
extension_db_drop_warning:
|
||||
"Stai per rimuovere tutti i dati per l'estensione. Digita il nome dell'estensione per continuare:",
|
||||
extension_min_lnbits_version:
|
||||
'Questa versione richiede almeno la versione LNbits',
|
||||
payment_hash: 'Hash del pagamento',
|
||||
fee: 'Tariffa',
|
||||
amount: 'Importo',
|
||||
tag: 'Etichetta',
|
||||
unit: 'Unità',
|
||||
description: 'Descrizione',
|
||||
expiry: 'Scadenza',
|
||||
webhook: 'Webhook',
|
||||
prova_di_pagamento: 'Prova di pagamento'
|
||||
payment_proof: 'Prova di pagamento',
|
||||
update_available: 'Aggiornamento %{version} disponibile!',
|
||||
latest_update: 'Sei sulla versione più recente %{version}.',
|
||||
notifications: 'Notifiche',
|
||||
no_notifications: 'Nessuna notifica',
|
||||
notifications_disabled: 'Le notifiche di stato di LNbits sono disattivate.',
|
||||
enable_notifications: 'Attiva le notifiche',
|
||||
enable_notifications_desc:
|
||||
'Se attivato, recupererà gli ultimi aggiornamenti sullo stato di LNbits, come incidenti di sicurezza e aggiornamenti.',
|
||||
enable_killswitch: 'Attiva Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Se attivato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se LNbits invia un segnale di killswitch. Dovrai attivare manualmente dopo un aggiornamento.',
|
||||
killswitch_interval: 'Intervallo Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNBits dalla fonte di stato (in minuti).',
|
||||
enable_watchdog: 'Attiva Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se abilitato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se il tuo saldo è inferiore al saldo LNbits. Dovrai abilitarlo manualmente dopo un aggiornamento.',
|
||||
watchdog_interval: 'Intervallo Watchdog',
|
||||
watchdog_interval_desc:
|
||||
'Quanto spesso il task in background dovrebbe controllare un segnale di killswitch nel delta del watchdog [node_balance - lnbits_balance] (in minuti).',
|
||||
watchdog_delta: 'Guardiano Delta',
|
||||
watchdog_delta_desc:
|
||||
"Limite prima che l'interruttore di sicurezza modifichi la fonte di finanziamento in VoidWallet [lnbits_balance - node_balance > delta]",
|
||||
status: 'Stato',
|
||||
notification_source: 'Sorgente di notifica',
|
||||
notification_source_label:
|
||||
'URL sorgente (utilizzare solo la fonte ufficiale di stato LNbits e fonti di cui ti puoi fidare)',
|
||||
more: 'più',
|
||||
less: 'meno',
|
||||
releases: 'Pubblicazioni',
|
||||
killswitch: 'Interruttore di spegnimento',
|
||||
watchdog: 'Cane da guardia',
|
||||
server_logs: 'Registri del server',
|
||||
ip_blocker: 'Blocco IP',
|
||||
security: 'Sicurezza',
|
||||
security_tools: 'Strumenti di sicurezza',
|
||||
block_access_hint: "Blocca l'accesso per IP",
|
||||
allow_access_hint:
|
||||
"Consenti l'accesso per IP (sovrascriverà gli IP bloccati)",
|
||||
enter_ip: "Inserisci l'IP e premi invio",
|
||||
rate_limiter: 'Limitatore di frequenza',
|
||||
wallet_limiter: 'Limitatore del Portafoglio',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Prelievo massimo giornaliero dal portafoglio in sats (0 per disabilitare)',
|
||||
wallet_max_ballance:
|
||||
'Saldo massimo del portafoglio in sats (0 per disabilitare)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minuti e secondi tra transazioni per portafoglio (0 per disabilitare)',
|
||||
number_of_requests: 'Numero di richieste',
|
||||
time_unit: 'Unità di tempo',
|
||||
minute: 'minuto',
|
||||
second: 'secondo',
|
||||
hour: 'ora',
|
||||
disable_server_log: 'Disabilita Registro Server',
|
||||
enable_server_log: 'Attiva Registro Server',
|
||||
coming_soon: 'Caratteristica in arrivo prossimamente',
|
||||
session_has_expired:
|
||||
'La tua sessione è scaduta. Per favore, effettua nuovamente il login.',
|
||||
instant_access_question: 'Vuoi accesso immediato?',
|
||||
login_with_user_id: 'Accedi con ID utente',
|
||||
or: 'oppure',
|
||||
create_new_wallet: 'Crea nuovo portafoglio',
|
||||
login_to_account: 'Accedi al tuo account',
|
||||
create_account: 'Crea un account',
|
||||
account_settings: "Impostazioni dell'account",
|
||||
signin_with_google: 'Accedi con Google',
|
||||
signin_with_github: 'Accedi con GitHub',
|
||||
signin_with_keycloak: 'Accedi con Keycloak',
|
||||
username_or_email: 'Nome utente o Email',
|
||||
password: 'Password',
|
||||
password_config: 'Configurazione della password',
|
||||
password_repeat: 'Ripeti la password',
|
||||
change_password: 'Cambia Password',
|
||||
set_password: 'Imposta password',
|
||||
invalid_password: 'La password deve contenere almeno 8 caratteri',
|
||||
login: 'Accesso',
|
||||
register: 'Registrati',
|
||||
username: 'Nome utente',
|
||||
user_id: 'ID utente',
|
||||
email: 'Email',
|
||||
first_name: 'Nome',
|
||||
last_name: 'Cognome',
|
||||
picture: 'Immagine',
|
||||
verify_email: 'Verifica email con',
|
||||
account: 'Conto',
|
||||
update_account: 'Aggiorna Account',
|
||||
invalid_username: 'Nome utente non valido',
|
||||
auth_provider: 'Provider di Autenticazione',
|
||||
my_account: 'Il mio account',
|
||||
back: 'Indietro',
|
||||
logout: 'Esci',
|
||||
look_and_feel: 'Aspetto e Comportamento',
|
||||
language: 'Lingua',
|
||||
color_scheme: 'Schema dei colori'
|
||||
}
|
||||
|
||||
+139
-11
@@ -1,11 +1,28 @@
|
||||
window.localisation.jp = {
|
||||
confirm: 'はい',
|
||||
server: 'サーバー',
|
||||
theme: 'テーマ',
|
||||
funding: '資金調達',
|
||||
users: 'ユーザー',
|
||||
apps: 'アプリ',
|
||||
channels: 'チャンネル',
|
||||
transactions: 'トランザクション',
|
||||
dashboard: 'ダッシュボード',
|
||||
node: 'ノード',
|
||||
total_capacity: '合計容量',
|
||||
avg_channel_size: '平均チャンネルサイズ',
|
||||
biggest_channel_size: '最大チャネルサイズ',
|
||||
smallest_channel_size: '最小チャンネルサイズ',
|
||||
number_of_channels: 'チャンネル数',
|
||||
active_channels: 'アクティブチャンネル',
|
||||
connect_peer: 'ピアを接続',
|
||||
connect: '接続',
|
||||
open_channel: 'オープンチャンネル',
|
||||
open: '開く',
|
||||
close_channel: 'チャンネルを閉じる',
|
||||
close: '閉じる',
|
||||
restart: 'サーバーを再起動する',
|
||||
save: '保存',
|
||||
unit: '単位',
|
||||
save_tooltip: '変更を保存する',
|
||||
topup: 'トップアップ',
|
||||
topup_wallet: 'ウォレットをトップアップする',
|
||||
@@ -18,7 +35,7 @@ window.localisation.jp = {
|
||||
name_your_wallet: 'あなたのウォレットの名前 %{name}',
|
||||
paste_invoice_label: '請求書を貼り付けてください',
|
||||
lnbits_description:
|
||||
'簡単にインストールでき、軽量で、LNbitsは現在LND、Core Lightning、OpenNode、LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
|
||||
'簡単にインストールでき、軽量で、LNbitsは現在LND、Core Lightning、OpenNode、Alby, ZBD, LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
|
||||
export_to_phone: '電話にエクスポート',
|
||||
export_to_phone_desc:
|
||||
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
|
||||
@@ -29,26 +46,38 @@ window.localisation.jp = {
|
||||
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
|
||||
rename_wallet: 'ウォレットの名前を変更',
|
||||
update_name: '名前を更新',
|
||||
fiat_tracking: 'フィアット追跡',
|
||||
currency: '通貨',
|
||||
update_currency: '通貨を更新する',
|
||||
press_to_claim: 'クレームするには押してください',
|
||||
donate: '寄付',
|
||||
view_github: 'GitHubで表示',
|
||||
voidwallet_active: 'Voidwalletアクティブ',
|
||||
use_with_caution: '注意して使用してください',
|
||||
toggle_dark_mode: 'ダークモードを切り替える',
|
||||
use_with_caution:
|
||||
'注意して使用してください - %{name} ウォレットはまだベータ版です',
|
||||
service_fee: '取引ごとのサービス手数料: %{amount} %',
|
||||
service_fee_max: '取引手数料:%{amount}%(最大%{max}サトシ)',
|
||||
service_fee_tooltip: 'LNbitsサーバー管理者が発生する送金ごとの手数料',
|
||||
toggle_darkmode: 'ダークモードを切り替える',
|
||||
payment_reactions: '支払いの反応',
|
||||
view_swagger_docs: 'Swaggerドキュメントを表示',
|
||||
api_docs: 'APIドキュメント',
|
||||
api_keys_api_docs: 'ノードURL、APIキー、APIドキュメント',
|
||||
lnbits_version: 'LNbits バージョン',
|
||||
runs_on: 'で実行',
|
||||
credit_hint:
|
||||
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
|
||||
credit_label: 'クレジットカード',
|
||||
credit_label: '%{denomination} をクレジットに',
|
||||
paste: '貼り付け',
|
||||
paste_from_clipboard: 'クリップボードから貼り付け',
|
||||
paste_request: 'リクエストを貼り付ける',
|
||||
create_invoice: '請求書を作成する',
|
||||
camera_tooltip: 'QRコードを読み取る',
|
||||
export_csv: 'CSVでエクスポート',
|
||||
transactions: 'トランザクション',
|
||||
chart_tooltip: 'チャートを表示するには、グラフの上にカーソルを合わせます',
|
||||
pending: '保留中',
|
||||
copy_invoice: '請求書をコピー',
|
||||
close: '閉じる',
|
||||
withdraw_from: '出金',
|
||||
cancel: 'キャンセル',
|
||||
scan: 'スキャン',
|
||||
read: '読む',
|
||||
@@ -71,19 +100,18 @@ window.localisation.jp = {
|
||||
disclaimer_dialog:
|
||||
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。ウォレットを削除する前に、ウォレットをエクスポートしてください。',
|
||||
no_transactions: 'トランザクションはありません',
|
||||
manage_server: 'サーバーを管理する',
|
||||
manage: '管理',
|
||||
extensions: '拡張機能',
|
||||
no_extensions: '拡張機能はありません',
|
||||
created: '作成済み',
|
||||
search_extensions: '検索拡張機能',
|
||||
warning: '警告',
|
||||
manage: '管理',
|
||||
repository: 'リポジトリ',
|
||||
confirm_continue: '続行してもよろしいですか?',
|
||||
manage_extension_details: '拡張機能のインストール/アンインストール',
|
||||
install: 'インストール',
|
||||
uninstall: 'アンインストール',
|
||||
open: '開く',
|
||||
drop_db: 'データを削除',
|
||||
enable: '有効',
|
||||
enable_extension_details: '現在のユーザーの拡張機能を有効にする',
|
||||
disable: '無効',
|
||||
@@ -97,6 +125,7 @@ window.localisation.jp = {
|
||||
all: 'すべて',
|
||||
only_admins_can_install:
|
||||
'(管理者アカウントのみが拡張機能をインストールできます)',
|
||||
admin_only: '管理者のみ',
|
||||
new_version: '新しいバージョン',
|
||||
extension_depends_on: '依存先:',
|
||||
extension_rating_soon: '評価は近日公開',
|
||||
@@ -104,13 +133,112 @@ window.localisation.jp = {
|
||||
extension_uninstall_warning:
|
||||
'すべてのユーザーの拡張機能を削除しようとしています.',
|
||||
uninstall_confirm: 'はい、アンインストールします',
|
||||
extension_db_drop_info:
|
||||
'エクステンションのすべてのデータが完全に削除されます。この操作を元に戻す方法はありません!',
|
||||
extension_db_drop_warning:
|
||||
'エクステンションのすべてのデータを削除しようとしています。続行するには、エクステンションの名前を入力してください:',
|
||||
extension_min_lnbits_version:
|
||||
'このリリースには少なくとも LNbits バージョンが必要です',
|
||||
payment_hash: '支払いハッシュ',
|
||||
fee: '料金',
|
||||
amount: '量',
|
||||
tag: 'タグ',
|
||||
unit: '単位',
|
||||
description: '説明',
|
||||
expiry: '有効期限',
|
||||
webhook: 'ウェブフック',
|
||||
payment_proof: '支払い証明'
|
||||
payment_proof: '支払い証明',
|
||||
update_available: 'アップデート%{version}が利用可能です!',
|
||||
latest_update: 'あなたは最新バージョン%{version}を使用しています。',
|
||||
notifications: '通知',
|
||||
no_notifications: '通知はありません',
|
||||
notifications_disabled: 'LNbitsステータス通知は無効です。',
|
||||
enable_notifications: '通知を有効にする',
|
||||
enable_notifications_desc:
|
||||
'有効にすると、セキュリティインシデントやアップデートのような最新のLNbitsステータス更新を取得します。',
|
||||
enable_killswitch: 'キルスイッチを有効にする',
|
||||
enable_killswitch_desc:
|
||||
'有効にすると、LNbitsからキルスイッチ信号が送信された場合に自動的に資金源をVoidWalletに切り替えます。更新後には手動で有効にする必要があります。',
|
||||
killswitch_interval: 'キルスイッチ間隔',
|
||||
killswitch_interval_desc:
|
||||
'バックグラウンドタスクがステータスソースからLNBitsキルスイッチ信号を確認する頻度(分単位)。',
|
||||
enable_watchdog: 'ウォッチドッグを有効にする',
|
||||
enable_watchdog_desc:
|
||||
'有効にすると、残高がLNbitsの残高より少ない場合に、資金源を自動的にVoidWalletに変更します。アップデート後は手動で有効にする必要があります。',
|
||||
watchdog_interval: 'ウォッチドッグ・インターバル',
|
||||
watchdog_interval_desc:
|
||||
'バックグラウンドタスクがウォッチドッグデルタ[node_balance - lnbits_balance]でキルスイッチシグナルを確認する頻度(分単位)。',
|
||||
watchdog_delta: 'ウォッチドッグデルタ',
|
||||
watchdog_delta_desc:
|
||||
'キルスイッチが資金源をVoidWalletに変更する前の限界 [lnbits_balance - node_balance > delta]',
|
||||
status: 'ステータス',
|
||||
notification_source: '通知ソース',
|
||||
notification_source_label:
|
||||
'ソースURL(公式のLNbitsステータスソースのみを使用し、信頼できるソースのみを利用してください)',
|
||||
more: 'より多くの',
|
||||
less: '少ない',
|
||||
releases: 'リリース',
|
||||
killswitch: 'キルスイッチ',
|
||||
watchdog: 'ウォッチドッグ',
|
||||
server_logs: 'サーバーログ',
|
||||
ip_blocker: 'IPブロッカー',
|
||||
security: 'セキュリティ',
|
||||
security_tools: 'セキュリティツール',
|
||||
block_access_hint: 'IPによるアクセスをブロック',
|
||||
allow_access_hint:
|
||||
'IPによるアクセスを許可する(ブロックされたIPを上書きします)',
|
||||
enter_ip: 'IPを入力してエンターキーを押してください',
|
||||
rate_limiter: 'レートリミッター',
|
||||
wallet_limiter: 'ウォレットリミッター',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'1日あたりの最大ウォレット出金額をsatsで入力してください(0 で無効)。',
|
||||
wallet_max_ballance: 'ウォレットの最大残高(sats)(0は無効)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'トランザクション間の最小秒数(ウォレットごと)(0は無効)',
|
||||
number_of_requests: 'リクエストの数',
|
||||
time_unit: '時間単位',
|
||||
minute: '分',
|
||||
second: '秒',
|
||||
hour: '時間',
|
||||
disable_server_log: 'サーバーログを無効にする',
|
||||
enable_server_log: 'サーバーログを有効にする',
|
||||
coming_soon: '機能は間もなく登場します',
|
||||
session_has_expired:
|
||||
'あなたのセッションは期限切れです。もう一度ログインしてください。',
|
||||
instant_access_question: '即時アクセスをご希望ですか?',
|
||||
login_with_user_id: 'ユーザーIDでログイン',
|
||||
or: 'または',
|
||||
create_new_wallet: '新しいウォレットを作成',
|
||||
login_to_account: 'アカウントにログインしてください',
|
||||
create_account: 'アカウントを作成',
|
||||
account_settings: 'アカウント設定',
|
||||
signin_with_google: 'Googleでサインイン',
|
||||
signin_with_github: 'GitHubでサインイン',
|
||||
signin_with_keycloak: 'Keycloakでサインイン',
|
||||
username_or_email: 'ユーザー名またはメールアドレス',
|
||||
password: 'パスワード',
|
||||
password_config: 'パスワード設定',
|
||||
password_repeat: 'パスワードの再入力',
|
||||
change_password: 'パスワードを変更',
|
||||
set_password: 'パスワードを設定',
|
||||
invalid_password: 'パスワードは少なくとも8文字必要です',
|
||||
login: 'ログイン',
|
||||
register: '登録',
|
||||
username: 'ユーザー名',
|
||||
user_id: 'ユーザーID',
|
||||
email: 'メール',
|
||||
first_name: '名',
|
||||
last_name: '姓',
|
||||
picture: '写真',
|
||||
verify_email: 'メールアドレスの確認を行ってください',
|
||||
account: 'アカウント',
|
||||
update_account: 'アカウントを更新',
|
||||
invalid_username: '無効なユーザー名',
|
||||
auth_provider: '認証プロバイダ',
|
||||
my_account: 'マイアカウント',
|
||||
back: '戻る',
|
||||
logout: 'ログアウト',
|
||||
look_and_feel: 'ルック・アンド・フィール',
|
||||
language: '言語',
|
||||
color_scheme: 'カラースキーム'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
window.localisation.kr = {
|
||||
confirm: '확인',
|
||||
server: '서버',
|
||||
theme: '테마',
|
||||
funding: '자금',
|
||||
users: '사용자',
|
||||
apps: '앱',
|
||||
channels: '채널',
|
||||
transactions: '거래 내역',
|
||||
dashboard: '현황판',
|
||||
node: '노드',
|
||||
total_capacity: '총 용량',
|
||||
avg_channel_size: '평균 채널 용량',
|
||||
biggest_channel_size: '가장 큰 채널 용량',
|
||||
smallest_channel_size: '가장 작은 채널 용량',
|
||||
number_of_channels: '채널 수',
|
||||
active_channels: '활성화된 채널',
|
||||
connect_peer: '피어 연결하기',
|
||||
connect: '연결하기',
|
||||
open_channel: '채널 개설하기',
|
||||
open: '개설',
|
||||
close_channel: '채널 폐쇄하기',
|
||||
close: '폐쇄',
|
||||
restart: '서버 재시작',
|
||||
save: '저장',
|
||||
save_tooltip: '변경 사항 저장',
|
||||
topup: '자금 추가',
|
||||
topup_wallet: '지갑에 자금 추가',
|
||||
topup_hint: '자금을 추가할 지갑의 ID를 넣어주세요',
|
||||
restart_tooltip: '변경 사항을 적용하려면 서버를 재시작해야 합니다.',
|
||||
add_funds_tooltip: '지갑에 자금을 추가합니다.',
|
||||
reset_defaults: '기본 설정으로 돌아가기',
|
||||
reset_defaults_tooltip:
|
||||
'설정했던 내용들을 모두 지우고, 기본 설정으로 돌아갑니다.',
|
||||
download_backup: '데이터베이스 백업 다운로드',
|
||||
name_your_wallet: '사용할 %{name}지갑의 이름을 정하세요',
|
||||
paste_invoice_label: '인보이스, 결제 요청, 혹은 lnurl 코드를 붙여넣으세요 *',
|
||||
lnbits_description:
|
||||
'설정이 쉽고 가벼운 LNBits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다. 현재 지원하는 예산 자원의 형태는 LND, Core Lightning, OpenNode, Alby, ZBD, LNPay, 그리고 다른 LNBits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNBits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNBits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNBits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNBits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
|
||||
export_to_phone: 'QR 코드를 이용해 모바일 기기로 내보내기',
|
||||
export_to_phone_desc:
|
||||
'이 QR 코드는 선택된 지갑의 최대 접근 권한을 가진 전체 URL을 담고 있습니다. 스캔 후, 모바일 기기에서 지갑을 열 수 있습니다.',
|
||||
wallets: '지갑',
|
||||
add_wallet: '새로운 지갑을 추가합니다',
|
||||
delete_wallet: '지갑을 삭제합니다',
|
||||
delete_wallet_desc:
|
||||
'이 지갑은 삭제될 것이며, 삭제 시 지갑 내 자금은 복구가 불가능합니다.',
|
||||
rename_wallet: '지갑 이름 변경',
|
||||
update_name: '이름 변경하기',
|
||||
fiat_tracking: '법정통화 가격 표시',
|
||||
currency: '통화',
|
||||
update_currency: '통화 수정하기',
|
||||
press_to_claim: '비트코인을 수령하려면 눌러주세요',
|
||||
donate: '기부',
|
||||
view_github: 'GitHub 페이지 보기',
|
||||
voidwallet_active: 'VoidWallet이 활성화되었습니다! 결제가 불가능합니다.',
|
||||
use_with_caution: '주의하세요 - %{name} 지갑은 아직 BETA 단계입니다.',
|
||||
service_fee: '서비스 수수료: 거래액의 %{amount} %',
|
||||
service_fee_max: '서비스 수수료: 거래액의 %{amount} % (최대 %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
'지불 결제 시마다 LNBits 서버 관리자에게 납부되는 서비스 수수료',
|
||||
toggle_darkmode: '다크 모드 전환',
|
||||
payment_reactions: '결제 반응',
|
||||
view_swagger_docs: 'LNbits Swagger API 문서를 봅니다',
|
||||
api_docs: 'API 문서',
|
||||
api_keys_api_docs: '노드 URL, API 키와 API 문서',
|
||||
lnbits_version: 'LNbits 버전',
|
||||
runs_on: 'Runs on',
|
||||
credit_hint: '계정에 자금을 넣으려면 Enter를 눌러주세요',
|
||||
credit_label: '%{denomination} 단위로 충전하기',
|
||||
paste: '붙여넣기',
|
||||
paste_from_clipboard: '클립보드에서 붙여넣기',
|
||||
paste_request: '지불 요청 붙여넣기',
|
||||
create_invoice: '인보이스 생성하기',
|
||||
camera_tooltip: '카메라를 이용해서 인보이스/QR을 스캔하세요',
|
||||
export_csv: 'CSV 형태로 내보내기',
|
||||
chart_tooltip: '그래프로 보여주기',
|
||||
pending: '대기 중',
|
||||
copy_invoice: '인보이스 복사하기',
|
||||
withdraw_from: '출금',
|
||||
cancel: '취소',
|
||||
scan: '스캔',
|
||||
read: '분석하기',
|
||||
pay: '지불하기',
|
||||
memo: 'Memo',
|
||||
date: '일시',
|
||||
processing_payment: '결제 처리 중...',
|
||||
not_enough_funds: '자금이 부족합니다!',
|
||||
search_by_tag_memo_amount: '태그, memo, 수량으로 검색하기',
|
||||
invoice_waiting: '결제를 기다리는 인보이스',
|
||||
payment_received: '받은 결제액',
|
||||
payment_sent: '보낸 결제액',
|
||||
receive: '받기',
|
||||
send: '보내기',
|
||||
outgoing_payment_pending: '지불 대기 중',
|
||||
drain_funds: '자금 비우기',
|
||||
drain_funds_desc:
|
||||
'이는 선택된 지갑으로부터 모든 자금을 인출하는 LNURL-withdraw QR 코드입니다. 그 누구와도 공유하지 마세요. balanceCheck 및 balanceNotify 기능과 호환되며, 당신의 지갑은 첫 출금 이후로도 계속 자금을 끌어당기고 있을 수 있습니다.',
|
||||
i_understand: '이해하였습니다',
|
||||
copy_wallet_url: '지갑 URL 복사하기',
|
||||
disclaimer_dialog:
|
||||
'로그인 기능은 향후 업데이트를 통해 지원될 계획이지만, 현재로써는 이 페이지에 향후 다시 접속하기 위해 북마크 설정하는 것을 잊지 마세요! 이 서비스는 아직 BETA 과정에 있고, LNbits 개발자들은 자금 손실에 대해 전혀 책임을 지지 않습니다.',
|
||||
no_transactions: '아직 아무런 거래도 이루어지지 않았습니다',
|
||||
manage: '관리',
|
||||
extensions: '확장 기능',
|
||||
no_extensions: '아직 설치된 확장 기능들이 없네요 :(',
|
||||
created: '생성됨',
|
||||
search_extensions: '확장 기능 검색하기',
|
||||
warning: '주의',
|
||||
repository: '저장소',
|
||||
confirm_continue: '정말로 계속할까요?',
|
||||
manage_extension_details: '확장 기능 설치/삭제하기',
|
||||
install: '설치',
|
||||
uninstall: '삭제',
|
||||
drop_db: '데이터 삭제',
|
||||
enable: '활성화',
|
||||
enable_extension_details: '현재 사용자 계정에 해당 확장 기능을 활성화합니다',
|
||||
disable: '비활성화',
|
||||
installed: '설치됨',
|
||||
activated: '작동됨',
|
||||
deactivated: '작동 중지',
|
||||
release_notes: '배포 노트',
|
||||
activate_extension_details:
|
||||
'사용자들의 확장 기능 사용 가능 여부를 결정합니다',
|
||||
featured: '추천',
|
||||
all: '전체',
|
||||
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
|
||||
admin_only: '관리자 전용',
|
||||
new_version: '새로운 버전',
|
||||
extension_depends_on: '의존성 존재:',
|
||||
extension_rating_soon: '평점 기능도 곧 구현됩니다',
|
||||
extension_installed_version: '설치된 버전',
|
||||
extension_uninstall_warning:
|
||||
'모든 사용자들로부터 이 확장 기능을 제거한다는 점에 유의하세요.',
|
||||
uninstall_confirm: '네, 삭제합니다',
|
||||
extension_db_drop_info:
|
||||
'해당 확장 기능의 모든 데이터가 영구적으로 삭제됩니다. 작업 수행 후에는 되돌릴 수 없습니다!',
|
||||
extension_db_drop_warning:
|
||||
'해당 확장 기능의 모든 데이터가 영구적으로 삭제될 겁니다. 계속하려면 확장 기능의 이름을 입력해주세요:',
|
||||
extension_min_lnbits_version:
|
||||
'이 배포 버전은 더 높은 버전의 lnbits가 설치되어 있어야 합니다.',
|
||||
payment_hash: '결제 해쉬값',
|
||||
fee: '수수료',
|
||||
amount: '액수',
|
||||
tag: '태그',
|
||||
unit: '단위',
|
||||
description: '상세',
|
||||
expiry: '만료',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Payment 증거',
|
||||
update_available: '%{version}으로 업데이트가 가능합니다.',
|
||||
latest_update: '이미 %{version} 버전으로 업데이트되었습니다.',
|
||||
notifications: '알림',
|
||||
no_notifications: '알림 없음',
|
||||
notifications_disabled: 'LNbits 상태 알림이 비활성화되었습니다.',
|
||||
enable_notifications: '알림 활성화',
|
||||
enable_notifications_desc:
|
||||
'활성화 시, 가장 최신의 보안 사고나 소프트웨어 업데이트 등의 LNbits 상황 업데이트를 불러옵니다.',
|
||||
enable_killswitch: '비상 정지 활성화',
|
||||
enable_killswitch_desc:
|
||||
'활성화 시, LNbits 메인 서버에서 비상 정지 신호를 보내면 자동으로 자금의 원천을 VoidWallet으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
|
||||
killswitch_interval: '비상 정지 시간 간격',
|
||||
killswitch_interval_desc:
|
||||
'LNbits 메인 서버에서 나오는 비상 정지 신호를 백그라운드 작업으로 얼마나 자주 확인할 것인지를 결정합니다. (분 단위)',
|
||||
enable_watchdog: '와치독 활성화',
|
||||
enable_watchdog_desc:
|
||||
'활성화 시, LNbits 잔금보다 당신의 잔금이 지정한 수준보다 더 낮아질 경우 자동으로 자금의 원천을 VoidWallet으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
|
||||
watchdog_interval: '와치독 시간 간격',
|
||||
watchdog_interval_desc:
|
||||
'와치독 델타 값을 기반으로 하여 당신의 LNbits 서버에서 나오는 비상 정지 신호를 백그라운드 작업으로 얼마나 자주 확인할 것인지를 결정합니다. (분 단위)',
|
||||
watchdog_delta: '와치독 델타',
|
||||
watchdog_delta_desc:
|
||||
'당신의 자금 원천을 VoidWallet으로 변경하기까지의 기준 값 [LNbits 잔액 - 노드 잔액 > 델타 값]',
|
||||
status: '상황',
|
||||
notification_source: '알림 메세지 출처',
|
||||
notification_source_label:
|
||||
'알림 메세지를 가져올 URL (공식 LNbits 상황판 출처나, 당신이 신뢰할 수 있는 출처만을 사용하세요)',
|
||||
more: '더 알아보기',
|
||||
less: '적게',
|
||||
releases: '배포 버전들',
|
||||
killswitch: '비상 정지',
|
||||
watchdog: '와치독',
|
||||
server_logs: '서버 로그',
|
||||
ip_blocker: 'IP 기반 차단기',
|
||||
security: '보안',
|
||||
security_tools: '보안 도구들',
|
||||
block_access_hint: 'IP 기준으로 접속 차단하기',
|
||||
allow_access_hint: 'IP 기준으로 접속 허용하기 (차단한 IP들을 무시합니다)',
|
||||
enter_ip: 'IP 주소를 입력하고 Enter를 눌러주세요',
|
||||
rate_limiter: '횟수로 제한하기',
|
||||
wallet_limiter: '지갑 제한기',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'일일 최대 지갑 출금액(sats) (0은 비활성화)',
|
||||
wallet_max_ballance: '지갑 최대 잔액(sats) (0은 비활성화)',
|
||||
wallet_limit_secs_between_trans: '지갑 당 거래 사이 최소 초 (0은 비활성화)',
|
||||
number_of_requests: '요청 횟수',
|
||||
time_unit: '시간 단위',
|
||||
minute: '분',
|
||||
second: '초',
|
||||
hour: '시간',
|
||||
disable_server_log: '서버 로깅 중단하기',
|
||||
enable_server_log: '서버 로깅 활성화하기',
|
||||
coming_soon: '곧 구현될 기능들입니다',
|
||||
session_has_expired: '세션 유효 기간이 만료되었습니다. 다시 로그인해 주세요.',
|
||||
instant_access_question: '즉시 액세스하시겠습니까?',
|
||||
login_with_user_id: '사용자 ID로 로그인',
|
||||
or: '또는',
|
||||
create_new_wallet: '새 지갑 만들기',
|
||||
login_to_account: '계정에 로그인하세요.',
|
||||
create_account: '계정 생성',
|
||||
account_settings: '계정 설정',
|
||||
signin_with_google: 'Google으로 로그인',
|
||||
signin_with_github: 'GitHub으로 로그인',
|
||||
signin_with_keycloak: 'Keycloak으로 로그인',
|
||||
username_or_email: '사용자 이름 또는 이메일',
|
||||
password: '비밀번호',
|
||||
password_config: '비밀번호 설정',
|
||||
password_repeat: '비밀번호 재입력',
|
||||
change_password: '비밀번호 변경',
|
||||
set_password: '비밀번호 설정',
|
||||
invalid_password: '비밀번호는 최소 8자 이상이어야 합니다',
|
||||
login: '로그인',
|
||||
register: '등록',
|
||||
username: '사용자 이름',
|
||||
user_id: '사용자 ID',
|
||||
email: '이메일',
|
||||
first_name: '성명',
|
||||
last_name: '성',
|
||||
picture: '사진',
|
||||
verify_email: '이메일을 인증하려면',
|
||||
account: '계정',
|
||||
update_account: '계정 업데이트',
|
||||
invalid_username: '잘못된 사용자 이름',
|
||||
auth_provider: '인증 제공자',
|
||||
my_account: '내 계정',
|
||||
back: '뒤로',
|
||||
logout: '로그아웃',
|
||||
look_and_feel: '외관과 느낌',
|
||||
language: '언어',
|
||||
color_scheme: '색상 구성'
|
||||
}
|
||||
+163
-7
@@ -1,8 +1,26 @@
|
||||
window.localisation.nl = {
|
||||
confirm: 'Ja',
|
||||
server: 'Server',
|
||||
theme: 'Thema',
|
||||
funding: 'Financiering',
|
||||
users: 'Gebruikers',
|
||||
apps: 'Apps',
|
||||
channels: 'Kanalen',
|
||||
transactions: 'Transacties',
|
||||
dashboard: 'Dashboard',
|
||||
node: 'Knooppunt',
|
||||
total_capacity: 'Totale capaciteit',
|
||||
avg_channel_size: 'Gem. Kanaalgrootte',
|
||||
biggest_channel_size: 'Grootste Kanaalgrootte',
|
||||
smallest_channel_size: 'Kleinste Kanaalgrootte',
|
||||
number_of_channels: 'Aantal kanalen',
|
||||
active_channels: 'Actieve Kanalen',
|
||||
connect_peer: 'Peer verbinden',
|
||||
connect: 'Verbinden',
|
||||
open_channel: 'Open Kanaal',
|
||||
open: 'Open',
|
||||
close_channel: 'Kanaal Sluiten',
|
||||
close: 'Sluiten',
|
||||
restart: 'Server opnieuw opstarten',
|
||||
save: 'Opslaan',
|
||||
save_tooltip: 'Sla uw wijzigingen op',
|
||||
@@ -19,7 +37,7 @@ window.localisation.nl = {
|
||||
name_your_wallet: 'Geef je %{name} portemonnee een naam',
|
||||
paste_invoice_label: 'Plak een factuur, betalingsverzoek of lnurl-code*',
|
||||
lnbits_description:
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, Core Lightning, OpenNode, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, Core Lightning, OpenNode, Alby, ZBD, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
export_to_phone: 'Exporteren naar telefoon met QR-code',
|
||||
export_to_phone_desc:
|
||||
'Deze QR-code bevat uw portemonnee-URL met volledige toegang. U kunt het vanaf uw telefoon scannen om uw portemonnee van daaruit te openen.',
|
||||
@@ -30,28 +48,39 @@ window.localisation.nl = {
|
||||
'Deze hele portemonnee wordt verwijderd, de fondsen worden NIET TERUGGEVONDEN.',
|
||||
rename_wallet: 'Portemonnee hernoemen',
|
||||
update_name: 'Naam bijwerken',
|
||||
fiat_tracking: 'Volgfunctie voor fiat-valuata',
|
||||
currency: 'Valuta',
|
||||
update_currency: 'Valuta bijwerken',
|
||||
press_to_claim: 'Druk om bitcoin te claimen',
|
||||
donate: 'Doneren',
|
||||
view_github: 'Bekijken op GitHub',
|
||||
voidwallet_active: 'VoidWallet is actief! Betalingen uitgeschakeld',
|
||||
use_with_caution:
|
||||
'GEBRUIK MET VOORZICHTIGHEID - %{name} portemonnee is nog in BETA',
|
||||
service_fee: 'Servicekosten: %{amount} % per transactie',
|
||||
service_fee_max:
|
||||
'Servicekosten: %{amount} % per transactie (max %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
'Transactiekosten in rekening gebracht door de LNbits serverbeheerder per uitgaande transactie',
|
||||
toggle_darkmode: 'Donkere modus aan/uit',
|
||||
payment_reactions: 'Betalingsreacties',
|
||||
view_swagger_docs: 'Bekijk LNbits Swagger API-documentatie',
|
||||
api_docs: 'API-documentatie',
|
||||
api_keys_api_docs: 'Node URL, API-sleutels en API-documentatie',
|
||||
lnbits_version: 'LNbits-versie',
|
||||
runs_on: 'Draait op',
|
||||
credit_hint: 'Druk op Enter om de rekening te crediteren',
|
||||
credit_label: '%{denomination} te crediteren',
|
||||
paste: 'Plakken',
|
||||
paste_from_clipboard: 'Plakken van klembord',
|
||||
paste_request: 'Verzoek plakken',
|
||||
create_invoice: 'Factuur aanmaken',
|
||||
camera_tooltip: 'Gebruik de camera om een factuur/QR-code te scannen',
|
||||
export_csv: 'Exporteer naar CSV',
|
||||
transactions: 'Transacties',
|
||||
chart_tooltip: 'Toon grafiek',
|
||||
pending: 'In behandeling',
|
||||
copy_invoice: 'Kopieer factuur',
|
||||
close: 'Sluiten',
|
||||
withdraw_from: 'Opnemen van',
|
||||
cancel: 'Annuleren',
|
||||
scan: 'Scannen',
|
||||
read: 'Lezen',
|
||||
@@ -66,7 +95,7 @@ window.localisation.nl = {
|
||||
payment_sent: 'Betaling verzonden',
|
||||
receive: 'ontvangen',
|
||||
send: 'versturen',
|
||||
voutgoing_payment_pending: 'Uitgaande betaling in behandeling',
|
||||
outgoing_payment_pending: 'Uitgaande betaling in behandeling',
|
||||
drain_funds: 'Geld opnemen',
|
||||
drain_funds_desc:
|
||||
'Dit is een LNURL-withdraw QR-code om alles uit deze portemonnee te halen. Deel deze code niet met anderen. Het is compatibel met balanceCheck en balanceNotify zodat jouw portemonnee continu geld kan blijven opnemen vanaf hier na de eerste opname.',
|
||||
@@ -75,17 +104,144 @@ window.localisation.nl = {
|
||||
disclaimer_dialog:
|
||||
'Inlogfunctionaliteit wordt uitgebracht in een toekomstige update. Zorg er nu voor dat je deze pagina als favoriet markeert om in de toekomst toegang te krijgen tot je portemonnee! Deze service is in BETA en we zijn niet verantwoordelijk voor mensen die de toegang tot hun fondsen verliezen.',
|
||||
no_transactions: 'Er zijn nog geen transacties gedaan',
|
||||
manage_extensions: 'Beheer extensies',
|
||||
manage_server: 'Beheer server',
|
||||
manage: 'Beheer',
|
||||
extensions: 'Extensies',
|
||||
no_extensions: 'Je hebt geen extensies geïnstalleerd :(',
|
||||
created: 'Aangemaakt',
|
||||
search_extensions: 'Zoekextensies',
|
||||
warning: 'Waarschuwing',
|
||||
repository: 'Repository',
|
||||
confirm_continue: 'Weet je zeker dat je wilt doorgaan?',
|
||||
manage_extension_details: 'Installeren/verwijderen van extensie',
|
||||
install: 'Installeren',
|
||||
uninstall: 'Deïnstalleren',
|
||||
drop_db: 'Gegevens verwijderen',
|
||||
enable: 'Inschakelen',
|
||||
enable_extension_details: 'Schakel extensie in voor huidige gebruiker',
|
||||
disable: 'Uitschakelen',
|
||||
installed: 'Geïnstalleerd',
|
||||
activated: 'Geactiveerd',
|
||||
deactivated: 'Gedeactiveerd',
|
||||
release_notes: 'Release-opmerkingen',
|
||||
activate_extension_details:
|
||||
'Maak extensie beschikbaar/niet beschikbaar voor gebruikers',
|
||||
featured: 'Uitgelicht',
|
||||
all: 'Alles',
|
||||
only_admins_can_install:
|
||||
'Alleen beheerdersaccounts kunnen extensies installeren',
|
||||
admin_only: 'Alleen beheerder',
|
||||
new_version: 'Nieuwe Versie',
|
||||
extension_depends_on: 'Afhankelijk van:',
|
||||
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
|
||||
extension_installed_version: 'Geïnstalleerde versie',
|
||||
extension_uninstall_warning:
|
||||
'U staat op het punt de extensie voor alle gebruikers te verwijderen.',
|
||||
uninstall_confirm: 'Ja, de-installeren',
|
||||
extension_db_drop_info:
|
||||
'Alle gegevens voor de extensie zullen permanent worden verwijderd. Er is geen manier om deze bewerking ongedaan te maken!',
|
||||
extension_db_drop_warning:
|
||||
'U staat op het punt alle gegevens voor de extensie te verwijderen. Typ de naam van de extensie om door te gaan:',
|
||||
extension_min_lnbits_version: 'Deze release vereist ten minste LNbits-versie',
|
||||
payment_hash: 'Betalings-hash',
|
||||
fee: 'Kosten',
|
||||
amount: 'Bedrag',
|
||||
tag: 'Label',
|
||||
unit: 'Eenheid',
|
||||
description: 'Beschrijving',
|
||||
expiry: 'Vervaldatum',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Betalingsbewijs'
|
||||
payment_proof: 'Betalingsbewijs',
|
||||
update_available: 'Update %{version} beschikbaar!',
|
||||
latest_update: 'U bent op de nieuwste versie %{version}.',
|
||||
notifications: 'Meldingen',
|
||||
no_notifications: 'Geen meldingen',
|
||||
notifications_disabled: 'LNbits-statusmeldingen zijn uitgeschakeld.',
|
||||
enable_notifications: 'Schakel meldingen in',
|
||||
enable_notifications_desc:
|
||||
'Indien ingeschakeld zal het de laatste LNbits Status updates ophalen, zoals veiligheidsincidenten en updates.',
|
||||
enable_killswitch: 'Activeer Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Indien ingeschakeld, zal het uw financieringsbron automatisch wijzigen naar VoidWallet als LNbits een killswitch-signaal verzendt. U zult het na een update handmatig moeten inschakelen.',
|
||||
killswitch_interval: 'Uitschakelschakelaar-interval',
|
||||
killswitch_interval_desc:
|
||||
'Hoe vaak de achtergrondtaak moet controleren op het LNBits killswitch signaal van de statusbron (in minuten).',
|
||||
enable_watchdog: 'Inschakelen Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Indien ingeschakeld, wordt uw betaalbron automatisch gewijzigd naar VoidWallet als uw saldo lager is dan het saldo van LNbits. U zult dit na een update handmatig moeten inschakelen.',
|
||||
watchdog_interval: 'Watchdog-interval',
|
||||
watchdog_interval_desc:
|
||||
'Hoe vaak de achtergrondtaak moet controleren op een killswitch signaal in het watchdog verschil [node_balance - lnbits_balance] (in minuten).',
|
||||
watchdog_delta: 'Waakhond Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limiet voordat de killswitch de financieringsbron verandert naar VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Status',
|
||||
notification_source: 'Notificatiebron',
|
||||
notification_source_label:
|
||||
'Bron-URL (gebruik alleen de officiële LNbits-statusbron en bronnen die u vertrouwt)',
|
||||
more: 'meer',
|
||||
less: 'minder',
|
||||
releases: 'Uitgaven',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Waakhond',
|
||||
server_logs: 'Serverlogboeken',
|
||||
ip_blocker: 'IP-blokkering',
|
||||
security: 'Beveiliging',
|
||||
security_tools: 'Beveiligingstools',
|
||||
block_access_hint: 'Toegang blokkeren per IP',
|
||||
allow_access_hint:
|
||||
"Toegang verlenen op basis van IP (zal geblokkeerde IP's overschrijven)",
|
||||
enter_ip: 'Voer IP in en druk op enter',
|
||||
rate_limiter: 'Snelheidsbegrenzer',
|
||||
wallet_limiter: 'Portemonnee Limietsteller',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximale dagelijkse opname van wallet in sats (0 om uit te schakelen)',
|
||||
wallet_max_ballance:
|
||||
'Maximale portefeuillesaldo in sats (0 om uit te schakelen)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min seconden tussen transacties per portemonnee (0 om uit te schakelen)',
|
||||
number_of_requests: 'Aantal verzoeken',
|
||||
time_unit: 'Tijdeenheid',
|
||||
minute: 'minuut',
|
||||
second: 'seconde',
|
||||
hour: 'uur',
|
||||
disable_server_log: 'Serverlog uitschakelen',
|
||||
enable_server_log: 'Activeer Serverlog',
|
||||
coming_soon: 'Functie binnenkort beschikbaar',
|
||||
session_has_expired: 'Uw sessie is verlopen. Log alstublieft opnieuw in.',
|
||||
instant_access_question: 'Wil je directe toegang?',
|
||||
login_with_user_id: 'Inloggen met gebruikers-ID',
|
||||
or: 'of',
|
||||
create_new_wallet: 'Nieuwe portemonnee aanmaken',
|
||||
login_to_account: 'Log in op je account',
|
||||
create_account: 'Account aanmaken',
|
||||
account_settings: 'Accountinstellingen',
|
||||
signin_with_google: 'Inloggen met Google',
|
||||
signin_with_github: 'Inloggen met GitHub',
|
||||
signin_with_keycloak: 'Inloggen met Keycloak',
|
||||
username_or_email: 'Gebruikersnaam of e-mail',
|
||||
password: 'Wachtwoord',
|
||||
password_config: 'Wachtwoordconfiguratie',
|
||||
password_repeat: 'Wachtwoord herhalen',
|
||||
change_password: 'Wachtwoord wijzigen',
|
||||
set_password: 'Wachtwoord instellen',
|
||||
invalid_password: 'Wachtwoord moet ten minste 8 tekens bevatten',
|
||||
login: 'Inloggen',
|
||||
register: 'Registreren',
|
||||
username: 'Gebruikersnaam',
|
||||
user_id: 'Gebruikers-ID',
|
||||
email: 'E-mail',
|
||||
first_name: 'Voornaam',
|
||||
last_name: 'Achternaam',
|
||||
picture: 'Foto',
|
||||
verify_email: 'E-mail verifiëren met',
|
||||
account: 'Account',
|
||||
update_account: 'Account bijwerken',
|
||||
invalid_username: 'Ongeldige gebruikersnaam',
|
||||
auth_provider: 'Auth Provider',
|
||||
my_account: 'Mijn Account',
|
||||
back: 'Terug',
|
||||
logout: 'Afmelden',
|
||||
look_and_feel: 'Uiterlijk en gedrag',
|
||||
language: 'Taal',
|
||||
color_scheme: 'Kleurenschema'
|
||||
}
|
||||
|
||||
+158
-6
@@ -1,8 +1,26 @@
|
||||
window.localisation.pi = {
|
||||
confirm: 'Aye',
|
||||
server: 'Cap`n',
|
||||
theme: 'Theme',
|
||||
funding: 'Funding',
|
||||
users: 'Buccaneers',
|
||||
apps: 'Arrrrplications',
|
||||
channels: 'Channels',
|
||||
transactions: 'Pirate Transactions and loot',
|
||||
dashboard: 'Arrr-board',
|
||||
node: 'Node',
|
||||
total_capacity: 'Total Capacity',
|
||||
avg_channel_size: 'Avg. Channel Size',
|
||||
biggest_channel_size: 'Largest Bilge Size',
|
||||
smallest_channel_size: 'Smallest Channel Size',
|
||||
number_of_channels: "Nummer o' Channels",
|
||||
active_channels: 'Active Channels',
|
||||
connect_peer: 'Connect Peer',
|
||||
connect: 'Connect',
|
||||
open_channel: 'Open Channel',
|
||||
open: 'Open yer hatches',
|
||||
close_channel: 'Shut Yer Gob Channel',
|
||||
close: 'Batten down the hatches, we be closin',
|
||||
restart: 'Arr, restart Cap`n',
|
||||
save: 'Bury Treasure',
|
||||
save_tooltip: 'Bury yer changes, matey',
|
||||
@@ -18,7 +36,7 @@ window.localisation.pi = {
|
||||
name_your_wallet: 'Name yer %{name} treasure chest',
|
||||
paste_invoice_label: 'Paste a booty, payment request or lnurl code, matey!',
|
||||
lnbits_description:
|
||||
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
|
||||
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, Alby, ZBD, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
|
||||
export_to_phone: 'Export to Phone with QR Code, me hearties',
|
||||
export_to_phone_desc:
|
||||
'This QR code contains yer chest URL with full access. Ye can scan it from yer phone to open yer chest from there, arr!',
|
||||
@@ -29,28 +47,38 @@ window.localisation.pi = {
|
||||
'This whole chest will be scuttled, the booty will be UNRECOVERABLE. Aye, be warned!',
|
||||
rename_wallet: 'Rename the Chest, me hearty',
|
||||
update_name: 'Update name like a captain',
|
||||
fiat_tracking: "Trackin' o' the treasure",
|
||||
currency: "Curr'nsey",
|
||||
update_currency: 'Update doubloons',
|
||||
press_to_claim: 'Press to claim gold doubloons, matey!',
|
||||
donate: 'Donate like a true pirate!',
|
||||
view_github: 'View on GitHub and find treasures',
|
||||
voidwallet_active: 'VoidWallet be active! Payments disabled',
|
||||
use_with_caution:
|
||||
'USE WITH CAUTION - %{name} chest be still in BETA. Aye, be careful!',
|
||||
service_fee: 'Service fee: %{amount} % per transaction',
|
||||
service_fee_max: 'Service fee: %{amount} % per transaction (max %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
"Service fee charged by the LNbits server admin per goin' transaction",
|
||||
toggle_darkmode: 'Toggle Dark Mode, arr!',
|
||||
payment_reactions: 'Payment Reactions',
|
||||
view_swagger_docs: 'View LNbits Swagger API docs and learn the secrets',
|
||||
api_docs: 'API docs for the scallywags',
|
||||
api_keys_api_docs: 'Node URL, API keys and API docs',
|
||||
lnbits_version: 'LNbits version, arr!',
|
||||
runs_on: 'Runs on, matey',
|
||||
credit_hint: 'Press Enter to credit account and make it richer',
|
||||
credit_label: '%{denomination} to credit, arr!',
|
||||
paste: 'Stow',
|
||||
paste_from_clipboard: 'Paste from clipboard',
|
||||
paste_request: 'Paste Request and find treasures',
|
||||
create_invoice: 'Create Booty Request and get rich, me hearties!',
|
||||
camera_tooltip: 'Use spyglass to scan a booty/QR, arr!',
|
||||
export_csv: 'Export to CSV and keep track of the booty',
|
||||
transactions: 'Pirate Transactions and loot',
|
||||
chart_tooltip: 'Show ye chart, me hearty',
|
||||
pending: 'Pendin like a ship at anchor',
|
||||
copy_invoice: 'Copy booty request, arrr',
|
||||
close: 'Batten down the hatches, we be closin',
|
||||
withdraw_from: 'Withdraw from',
|
||||
cancel: 'Abandon ship! We be retreatin',
|
||||
scan: 'Avast! Scan me beauty, arrr',
|
||||
read: 'Read it, if ye dare',
|
||||
@@ -75,18 +103,142 @@ window.localisation.pi = {
|
||||
disclaimer_dialog:
|
||||
'Login functionality to be released in a future update, for now, make sure ye bookmark this page for future access to your booty! This service be in BETA, and we hold no responsibility for people losing access to doubloons.',
|
||||
no_transactions: 'No transactions made yet, me hearties. Belay that!',
|
||||
manage_extensions: 'Manage Yer Extensions, ye landlubber',
|
||||
manage_server: 'Manage Yer Server, me hearty',
|
||||
manage: 'Manage, me hearty',
|
||||
extensions: 'Yer Extensions, ye scurvy dog',
|
||||
no_extensions:
|
||||
"Ye don't have any extensions installed, ye scallywag :(. Where be yer loot?",
|
||||
created: 'Created like a legend, savvy',
|
||||
search_extensions: 'Search fer extensions',
|
||||
warning: 'Avast',
|
||||
repository: 'Repository',
|
||||
confirm_continue: "Be ye sure ye want t' proceed?",
|
||||
manage_extension_details: 'Install/uninstall extension',
|
||||
install: 'Set sail',
|
||||
uninstall: 'Avaast',
|
||||
drop_db: 'Scuttle Data',
|
||||
enable: 'Enable',
|
||||
enable_extension_details: "Enable extension fer th' current user",
|
||||
disable: "Disablin'",
|
||||
installed: 'Installed',
|
||||
activated: 'Activated',
|
||||
deactivated: 'Deactivated',
|
||||
release_notes: 'Release Notes',
|
||||
activate_extension_details: 'Make extension available/unavailable fer users',
|
||||
featured: 'Featured',
|
||||
all: 'Arr',
|
||||
only_admins_can_install: '(Only admin accounts can install extensions)',
|
||||
admin_only: "Cap'n Only",
|
||||
new_version: 'New Version',
|
||||
extension_depends_on: 'Depends on:',
|
||||
extension_rating_soon: "Ratings a'comin' soon",
|
||||
extension_installed_version: 'Installed version',
|
||||
extension_uninstall_warning:
|
||||
"Ye be about t' remove th' extension fer all hands.",
|
||||
uninstall_confirm: 'Aye, Uninstall',
|
||||
extension_db_drop_info:
|
||||
"All data fer th' extension will be permanently deleted. There be no way to undo this operation!",
|
||||
extension_db_drop_warning:
|
||||
"Ye be about to scuttle all data fer th' extension. Please scribble th' extension name to continue:",
|
||||
extension_min_lnbits_version:
|
||||
"This release be needin' at least LNbits version",
|
||||
payment_hash: 'Payment Hash like a treasure map, arrr',
|
||||
fee: 'Fee like a toll to cross a strait, matey',
|
||||
amount: 'Amount of doubloons, arrr',
|
||||
tag: 'Tag',
|
||||
unit: 'Unit of measurement like a fathom, ye buccaneer',
|
||||
description: 'Description like a tale of adventure, arrr',
|
||||
expiry: 'Expiry like the food on a ship, ye landlubber',
|
||||
webhook: 'Webhook like a fishing line, arrr',
|
||||
payment_proof: 'Payment Proof like a seal of authenticity, argh'
|
||||
payment_proof: 'Payment Proof like a seal of authenticity, argh',
|
||||
update_available: 'Update %{version} available, me matey!',
|
||||
latest_update: "Ye be on th' latest version %{version}.",
|
||||
notifications: 'Notificashuns',
|
||||
no_notifications: "No noticin's",
|
||||
notifications_disabled: 'LNbits status notifications be disabled, arr!',
|
||||
enable_notifications: 'Enable Notifications',
|
||||
enable_notifications_desc:
|
||||
"If ye be allowin' it, it'll be fetchin' the latest LNbits Status updates, like security incidents and updates.",
|
||||
enable_killswitch: "Enabl' th' Killswitch",
|
||||
enable_killswitch_desc:
|
||||
"If enabled it'll be changin' yer fundin' source to VoidWallet automatically if LNbits sends out a killswitch signal, ye will. Ye'll be needin' t' enable manually after an update, arr.",
|
||||
killswitch_interval: 'Killswitch Interval',
|
||||
killswitch_interval_desc:
|
||||
"How oft th' background task should be checkin' fer th' LNBits killswitch signal from th' status source (in minutes).",
|
||||
enable_watchdog: 'Enable Seadog',
|
||||
enable_watchdog_desc:
|
||||
"If enabled, it will swap yer treasure source t' VoidWallet on its own if yer balance be lower than th' LNbits balance. Ye'll need t' enable by hand after an update.",
|
||||
watchdog_interval: 'Seadog Interval',
|
||||
watchdog_interval_desc:
|
||||
"How oft th' background task should be checkin' fer a killswitch signal in th' seadog delta [node_balance - lnbits_balance] (in minutes), arr.",
|
||||
watchdog_delta: 'Seadog Delta',
|
||||
watchdog_delta_desc:
|
||||
"Limit afore killswitch changes fundin' source to VoidWallet [lnbits_balance - node_balance > delta]",
|
||||
status: 'Status',
|
||||
notification_source: 'Notification Source',
|
||||
notification_source_label:
|
||||
"Source URL (only use th' official LNbits status source, and sources ye can trust)",
|
||||
more: "Arr, 'tis more.",
|
||||
less: "Arr, 'tis more fewer.",
|
||||
releases: 'Releases',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Seadog',
|
||||
server_logs: 'Server Logs',
|
||||
ip_blocker: 'IP Blockar',
|
||||
security: 'Securrrity',
|
||||
security_tools: 'Securrrity tools',
|
||||
block_access_hint: 'Block access by IP',
|
||||
allow_access_hint: 'Grant permission by IP (will override barred IPs)',
|
||||
enter_ip: 'Enter IP and hit enter',
|
||||
rate_limiter: 'Rate Limiter',
|
||||
wallet_limiter: 'Pouch Limitar',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Max daily wallet withdrawal in sats (0 ter disable)',
|
||||
wallet_max_ballance: 'Purse max heaviness in sats (0 fer scuttle)',
|
||||
wallet_limit_secs_between_trans:
|
||||
"Min secs 'tween transactions per wallet (0 to disable)",
|
||||
number_of_requests: "Number o' requests",
|
||||
time_unit: "time bein'",
|
||||
minute: 'minnit',
|
||||
second: 'second',
|
||||
hour: 'hour',
|
||||
disable_server_log: "Disabl' %{Server} Log",
|
||||
enable_server_log: 'Enable Server Log',
|
||||
coming_soon: "Feature comin' soon",
|
||||
session_has_expired: 'Yer session has expired. Please login again.',
|
||||
instant_access_question: "Be wantin' quick entry, aye?",
|
||||
login_with_user_id: 'Login with user ID',
|
||||
or: 'arr',
|
||||
create_new_wallet: 'Create New Wallet',
|
||||
login_to_account: 'Log in to yer account',
|
||||
create_account: 'Create account',
|
||||
account_settings: "Account Settin's",
|
||||
signin_with_google: "Sign in wit' Google",
|
||||
signin_with_github: "Sign in wit' GitHub",
|
||||
signin_with_keycloak: "Sign in wit' Keycloak",
|
||||
username_or_email: 'Usarrrname or Email',
|
||||
password: 'Passwarrd',
|
||||
password_config: 'Passwarrd Config',
|
||||
password_repeat: 'Passwarrd repeat',
|
||||
change_password: 'Change Passwarrd',
|
||||
set_password: 'Set yer Secret Code',
|
||||
invalid_password: "Passwarrd must be havin' at leest 8 charrracters",
|
||||
login: 'Log in',
|
||||
register: 'Sign on',
|
||||
username: 'Username',
|
||||
user_id: 'User ID',
|
||||
email: 'Email',
|
||||
first_name: 'Firrrst Name',
|
||||
last_name: 'Surname',
|
||||
picture: "pictur'",
|
||||
verify_email: 'Verify email with',
|
||||
account: 'Arrrccount',
|
||||
update_account: "Updatin' Arrrccount",
|
||||
invalid_username: 'Username be not valid, matey!',
|
||||
auth_provider: 'Auth Provider becometh Auth Provider, ye see?',
|
||||
my_account: 'Me Arrrccount',
|
||||
back: 'Return',
|
||||
logout: 'Log out yer session',
|
||||
look_and_feel: 'Look and Feel',
|
||||
language: 'Langwidge',
|
||||
color_scheme: 'Colour Scheme'
|
||||
}
|
||||
|
||||
+161
-6
@@ -1,8 +1,26 @@
|
||||
window.localisation.pl = {
|
||||
confirm: 'Tak',
|
||||
server: 'Serwer',
|
||||
theme: 'Motyw',
|
||||
funding: 'Finansowanie',
|
||||
users: 'Użytkownicy',
|
||||
apps: 'Aplikacje',
|
||||
channels: 'Kanały',
|
||||
transactions: 'Transakcje',
|
||||
dashboard: 'Panel kontrolny',
|
||||
node: 'Węzeł',
|
||||
total_capacity: 'Całkowita Pojemność',
|
||||
avg_channel_size: 'Średni rozmiar kanału',
|
||||
biggest_channel_size: 'Największy Rozmiar Kanału',
|
||||
smallest_channel_size: 'Najmniejszy Rozmiar Kanału',
|
||||
number_of_channels: 'Ilość kanałów',
|
||||
active_channels: 'Aktywne kanały',
|
||||
connect_peer: 'Połącz z węzłem równorzędnym',
|
||||
connect: 'Połącz',
|
||||
open_channel: 'Otwarty Kanał',
|
||||
open: 'Otwórz',
|
||||
close_channel: 'Zamknij kanał',
|
||||
close: 'Zamknij',
|
||||
restart: 'Restart serwera',
|
||||
save: 'Zapisz',
|
||||
save_tooltip: 'Zapisz zmiany',
|
||||
@@ -17,7 +35,7 @@ window.localisation.pl = {
|
||||
name_your_wallet: 'Nazwij swój portfel %{name}',
|
||||
paste_invoice_label: 'Wklej fakturę, żądanie zapłaty lub kod lnurl *',
|
||||
lnbits_description:
|
||||
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning, obecnie wspiera LND, Core Lightning, OpenNode, LNPay czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
|
||||
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning, obecnie wspiera LND, Core Lightning, OpenNode, Alby, ZBD, LNPay czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
|
||||
export_to_phone: 'Eksport kodu QR na telefon',
|
||||
export_to_phone_desc:
|
||||
'Ten kod QR zawiera adres URL Twojego portfela z pełnym dostępem do niego. Możesz go zeskanować na swoim telefonie aby otworzyć na nim ten portfel.',
|
||||
@@ -28,27 +46,38 @@ window.localisation.pl = {
|
||||
'Ten portfel zostanie usunięty, środków na nim zgromadzonych NIE BĘDZIE MOŻNA ODZYSKAĆ.',
|
||||
rename_wallet: 'Zmień nazwę portfela',
|
||||
update_name: 'Zaktualizuj nazwę',
|
||||
fiat_tracking: 'Śledzenie Fiata',
|
||||
currency: 'Waluta',
|
||||
update_currency: 'Aktualizuj walutę',
|
||||
press_to_claim: 'Naciśnij aby odebrać Bitcoiny',
|
||||
donate: 'Podaruj',
|
||||
view_github: 'Otwórz GitHub',
|
||||
voidwallet_active: 'VoidWallet jest aktywny! Płatności są niemożliwe',
|
||||
use_with_caution: 'KORZYSTAJ Z ROZWAGĄ - portfel %{name} jest w wersji BETA',
|
||||
service_fee: 'Opłata serwisowa: %{amount} % za transakcję',
|
||||
service_fee_max:
|
||||
'Opłata serwisowa: %{amount} % za transakcję (maks %{max} sat)',
|
||||
service_fee_tooltip:
|
||||
'Opłata serwisowa pobierana przez administratora serwera LNbits za każdą wychodzącą transakcję',
|
||||
toggle_darkmode: 'Tryb nocny',
|
||||
payment_reactions: 'Reakcje na płatność',
|
||||
view_swagger_docs: 'Dokumentacja Swagger API',
|
||||
api_docs: 'Dokumentacja API',
|
||||
api_keys_api_docs: 'Adres URL węzła, klucze API i dokumentacja API',
|
||||
lnbits_version: 'Wersja LNbits',
|
||||
runs_on: 'Działa na',
|
||||
credit_hint: 'Naciśnij Enter aby doładować konto',
|
||||
credit_label: '%{denomination} doładowanie',
|
||||
paste: 'Wklej',
|
||||
paste_from_clipboard: 'Wklej ze schowka',
|
||||
paste_request: 'Wklej żądanie',
|
||||
create_invoice: 'Utwórz fakturę',
|
||||
camera_tooltip: 'Użyj kamery aby zeskanować fakturę lub kod QR',
|
||||
export_csv: 'Eksport do CSV',
|
||||
transactions: 'Transakcje',
|
||||
chart_tooltip: 'Wykres',
|
||||
pending: 'W toku',
|
||||
copy_invoice: 'Skopiuj fakturę',
|
||||
close: 'Zamknij',
|
||||
withdraw_from: 'Wypłać z',
|
||||
cancel: 'Anuluj',
|
||||
scan: 'Skanuj',
|
||||
read: 'Odczytaj',
|
||||
@@ -72,17 +101,143 @@ window.localisation.pl = {
|
||||
disclaimer_dialog:
|
||||
'Funkcja logowania zostanie uruchomiona w przyszłości. Póki co upewnij się, że zapisałeś adres URL tej strony aby mieć dostęp do tego portfela. Nie udostępniaj adresu tej strony nikomu, kto nie ma mieć do tego portfela dostępu! Ta usługa działa w wersji BETA, nie odpowiadamy za utratę dostępu do środków przez osoby używające LNbits.',
|
||||
no_transactions: 'Brak transakcji',
|
||||
manage_extensions: 'Zarządzaj rozszerzeniami',
|
||||
manage_server: 'Zarządzaj serwerem',
|
||||
manage: 'Zarządzaj',
|
||||
extensions: 'Rozszerzenia',
|
||||
no_extensions: 'Nie masz zainstalowanych żadnych rozszerzeń :(',
|
||||
created: 'Utworzono',
|
||||
search_extensions: 'Szukaj rozszerzeń',
|
||||
warning: 'Ostrzeżenie',
|
||||
repository: 'Repozytorium',
|
||||
confirm_continue: 'Czy na pewno chcesz kontynuować?',
|
||||
manage_extension_details: 'Instaluj/odinstaluj rozszerzenie',
|
||||
install: 'Zainstaluj',
|
||||
uninstall: 'Odinstaluj',
|
||||
drop_db: 'Usuń dane',
|
||||
enable: 'Włącz',
|
||||
enable_extension_details: 'Włącz rozszerzenie dla aktualnego użytkownika',
|
||||
disable: 'Wyłącz',
|
||||
installed: 'Zainstalowano',
|
||||
activated: 'Aktywowany',
|
||||
deactivated: 'Dezaktywowany',
|
||||
release_notes: 'Informacje o wydaniu',
|
||||
activate_extension_details:
|
||||
'Udostępnij/nie udostępniaj rozszerzenia użytkownikom',
|
||||
featured: 'Polecane',
|
||||
all: 'Wszystko',
|
||||
only_admins_can_install:
|
||||
'Tylko konta administratorów mogą instalować rozszerzenia',
|
||||
admin_only: 'Tylko dla administratora',
|
||||
new_version: 'Nowa wersja',
|
||||
extension_depends_on: 'Zależy od:',
|
||||
extension_rating_soon: 'Oceny będą dostępne wkrótce',
|
||||
extension_installed_version: 'Zainstalowana wersja',
|
||||
extension_uninstall_warning:
|
||||
'Za chwilę usuniesz rozszerzenie dla wszystkich użytkowników.',
|
||||
uninstall_confirm: 'Tak, Odinstaluj',
|
||||
extension_db_drop_info:
|
||||
'Wszystkie dane dla rozszerzenia zostaną trwale usunięte. Nie ma sposobu, aby cofnąć tę operację!',
|
||||
extension_db_drop_warning:
|
||||
'Za chwilę usuniesz wszystkie dane dla rozszerzenia. Proszę wpisz nazwę rozszerzenia, aby kontynuować:',
|
||||
extension_min_lnbits_version: 'To wymaga przynajmniej wersji LNbits',
|
||||
payment_hash: 'Hash Płatności',
|
||||
fee: 'Opłata',
|
||||
amount: 'Wartość',
|
||||
tag: 'Etykieta',
|
||||
unit: 'Jednostka',
|
||||
description: 'Opis',
|
||||
expiry: 'Wygasa',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Potwierdzenie płatności'
|
||||
payment_proof: 'Potwierdzenie płatności',
|
||||
update_available: 'Aktualizacja %{version} dostępna!',
|
||||
latest_update: 'Korzystasz z najnowszej wersji %{version}.',
|
||||
notifications: 'Powiadomienia',
|
||||
no_notifications: 'Brak powiadomień',
|
||||
notifications_disabled: 'Powiadomienia o statusie LNbits są wyłączone.',
|
||||
enable_notifications: 'Włącz powiadomienia',
|
||||
enable_notifications_desc:
|
||||
'Jeśli ta opcja zostanie włączona, będzie pobierać najnowsze informacje o statusie LNbits, takie jak incydenty bezpieczeństwa i aktualizacje.',
|
||||
enable_killswitch: 'Włącz Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli LNbits wyśle sygnał wyłączający. Po aktualizacji będziesz musiał włączyć to ręcznie.',
|
||||
killswitch_interval: 'Interwał wyłącznika awaryjnego',
|
||||
killswitch_interval_desc:
|
||||
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego LNBits ze źródła statusu (w minutach).',
|
||||
enable_watchdog: 'Włącz Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli saldo jest niższe niż saldo LNbits. Po aktualizacji trzeba będzie włączyć ręcznie.',
|
||||
watchdog_interval: 'Interwał Watchdog',
|
||||
watchdog_interval_desc:
|
||||
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego w delcie strażnika [node_balance - lnbits_balance] (w minutach).',
|
||||
watchdog_delta: 'Strażnik Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limit przed aktywacją wyłącznika zmienia źródło finansowania na VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Stan',
|
||||
notification_source: 'Źródło powiadomień',
|
||||
notification_source_label:
|
||||
'Adres URL źródła (używaj tylko oficjalnego źródła statusu LNbits oraz źródeł, którym możesz zaufać)',
|
||||
more: 'więcej',
|
||||
less: 'mniej',
|
||||
releases: 'Wydania',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Pies gończy',
|
||||
server_logs: 'Dzienniki serwera',
|
||||
ip_blocker: 'Blokada IP',
|
||||
security: 'Bezpieczeństwo',
|
||||
security_tools: 'Narzędzia bezpieczeństwa',
|
||||
block_access_hint: 'Zablokuj dostęp przez IP',
|
||||
allow_access_hint:
|
||||
'Zezwól na dostęp przez IP (zignoruje zablokowane adresy IP)',
|
||||
enter_ip: 'Wpisz adres IP i naciśnij enter',
|
||||
rate_limiter: 'Ogranicznik Częstotliwości',
|
||||
wallet_limiter: 'Ogranicznik Portfela',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maksymalna dzienna wypłata z portfela w satoshi (0 aby wyłączyć)',
|
||||
wallet_max_ballance: 'Maksymalny stan portfela w satoshi (0 aby wyłączyć)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min sekund pomiędzy transakcjami na portfel (0 aby wyłączyć)',
|
||||
number_of_requests: 'Liczba żądań',
|
||||
time_unit: 'Jednostka czasu',
|
||||
minute: 'minuta',
|
||||
second: 'sekunda',
|
||||
hour: 'godzina',
|
||||
disable_server_log: 'Wyłącz log serwera',
|
||||
enable_server_log: 'Włącz dziennik serwera',
|
||||
coming_soon: 'Funkcja wkrótce będzie dostępna',
|
||||
session_has_expired: 'Twoja sesja wygasła. Proszę zaloguj się ponownie.',
|
||||
instant_access_question: 'Chcesz mieć natychmiastowy dostęp?',
|
||||
login_with_user_id: 'Zaloguj się za pomocą identyfikatora użytkownika',
|
||||
or: 'lub',
|
||||
create_new_wallet: 'Utwórz nowy portfel',
|
||||
login_to_account: 'Zaloguj się do swojego konta',
|
||||
create_account: 'Załóż konto',
|
||||
account_settings: 'Ustawienia konta',
|
||||
signin_with_google: 'Zaloguj się przez Google',
|
||||
signin_with_github: 'Zaloguj się przez GitHub',
|
||||
signin_with_keycloak: 'Zaloguj się przez Keycloak',
|
||||
username_or_email: 'Nazwa użytkownika lub Email',
|
||||
password: 'Hasło',
|
||||
password_config: 'Konfiguracja Hasła',
|
||||
password_repeat: 'Powtórz hasło',
|
||||
change_password: 'Zmień hasło',
|
||||
set_password: 'Ustaw hasło',
|
||||
invalid_password: 'Hasło musi zawierać co najmniej 8 znaków',
|
||||
login: 'Logowanie',
|
||||
register: 'Zarejestruj',
|
||||
username: 'Nazwa użytkownika',
|
||||
user_id: 'Identyfikator użytkownika',
|
||||
email: 'Email',
|
||||
first_name: 'Imię',
|
||||
last_name: 'Nazwisko',
|
||||
picture: 'Zdjęcie',
|
||||
verify_email: 'Zweryfikuj email za pomocą',
|
||||
account: 'Konto',
|
||||
update_account: 'Aktualizuj konto',
|
||||
invalid_username: 'Nieprawidłowa nazwa użytkownika',
|
||||
auth_provider: 'Dostawca uwierzytelniania',
|
||||
my_account: 'Moje Konto',
|
||||
back: 'Wstecz',
|
||||
logout: 'Wyloguj',
|
||||
look_and_feel: 'Wygląd i zachowanie',
|
||||
language: 'Język',
|
||||
color_scheme: 'Schemat kolorów'
|
||||
}
|
||||
|
||||
+161
-6
@@ -1,8 +1,26 @@
|
||||
window.localisation.pt = {
|
||||
confirm: 'Sim',
|
||||
server: 'Servidor',
|
||||
theme: 'Tema',
|
||||
funding: 'Financiamento',
|
||||
users: 'Usuários',
|
||||
apps: 'Aplicativos',
|
||||
channels: 'Canais',
|
||||
transactions: 'Transações',
|
||||
dashboard: 'Painel de Controle',
|
||||
node: 'Nó',
|
||||
total_capacity: 'Capacidade Total',
|
||||
avg_channel_size: 'Tamanho Médio do Canal',
|
||||
biggest_channel_size: 'Maior Tamanho do Canal',
|
||||
smallest_channel_size: 'Menor Tamanho de Canal',
|
||||
number_of_channels: 'Número de Canais',
|
||||
active_channels: 'Canais Ativos',
|
||||
connect_peer: 'Conectar Par',
|
||||
connect: 'Conectar',
|
||||
open_channel: 'Canal Aberto',
|
||||
open: 'Abrir',
|
||||
close_channel: 'Fechar Canal',
|
||||
close: 'Fechar',
|
||||
restart: 'Reiniciar servidor',
|
||||
save: 'Gravar',
|
||||
save_tooltip: 'Gravar as alterações',
|
||||
@@ -18,7 +36,7 @@ window.localisation.pt = {
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, Alby, ZBD, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
export_to_phone: 'Exportar para o telefone com código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
|
||||
@@ -29,27 +47,38 @@ window.localisation.pt = {
|
||||
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
|
||||
rename_wallet: 'Renomear carteira',
|
||||
update_name: 'Atualizar nome',
|
||||
fiat_tracking: 'Rastreamento Fiat',
|
||||
currency: 'Moeda',
|
||||
update_currency: 'Atualizar moeda',
|
||||
press_to_claim: 'Pressione para solicitar bitcoin',
|
||||
donate: 'Doar',
|
||||
view_github: 'Ver no GitHub',
|
||||
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
|
||||
use_with_caution: 'USE COM CAUTELA - a carteira %{name} ainda está em BETA',
|
||||
service_fee: 'Taxa de serviço: %{amount} % por transação',
|
||||
service_fee_max:
|
||||
'Taxa de serviço: %{amount} % por transação (máximo de %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
|
||||
toggle_darkmode: 'Alternar modo escuro',
|
||||
payment_reactions: 'Reações de Pagamento',
|
||||
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
|
||||
api_docs: 'Documentação da API',
|
||||
api_keys_api_docs: 'URL do Nó, chaves de API e documentação de API',
|
||||
lnbits_version: 'Versão do LNbits',
|
||||
runs_on: 'Executa em',
|
||||
credit_hint: 'Pressione Enter para creditar a conta',
|
||||
credit_label: '%{denomination} para creditar',
|
||||
paste: 'Colar',
|
||||
paste_from_clipboard: 'Colar da área de transferência',
|
||||
paste_request: 'Colar Pedido',
|
||||
create_invoice: 'Criar Fatura',
|
||||
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
|
||||
export_csv: 'Exportar para CSV',
|
||||
transactions: 'Transações',
|
||||
chart_tooltip: 'Mostrar gráfico',
|
||||
pending: 'Pendente',
|
||||
copy_invoice: 'Copiar fatura',
|
||||
close: 'Fechar',
|
||||
withdraw_from: 'Retirar de',
|
||||
cancel: 'Cancelar',
|
||||
scan: 'Escanear',
|
||||
read: 'Ler',
|
||||
@@ -73,17 +102,143 @@ window.localisation.pt = {
|
||||
disclaimer_dialog:
|
||||
'Funcionalidade de login a ser lançada numa atualização futura, por enquanto, certifique-se que marca esta página para acesso futuro à sua carteira! Este serviço está em BETA, e não nos responsabilizamos por pessoas que perderem o acesso aos fundos.',
|
||||
no_transactions: 'Ainda não foram feitas transações',
|
||||
manage_extensions: 'Gerir extensões',
|
||||
manage_server: 'Gerir servidor',
|
||||
manage: 'Gerir',
|
||||
extensions: 'Extensões',
|
||||
no_extensions: 'Não há nenhuma extensão instalada :(',
|
||||
created: 'Criado',
|
||||
search_extensions: 'Pesquisar extensões',
|
||||
warning: 'Aviso',
|
||||
repository: 'Repositório',
|
||||
confirm_continue: 'Tem certeza de que deseja continuar?',
|
||||
manage_extension_details: 'Instalar/desinstalar extensão',
|
||||
install: 'Instalar',
|
||||
uninstall: 'Desinstalar',
|
||||
drop_db: 'Remover Dados',
|
||||
enable: 'Ativar',
|
||||
enable_extension_details: 'Ativar extensão para o usuário atual',
|
||||
disable: 'Desativar',
|
||||
installed: 'Instalado',
|
||||
activated: 'Ativado',
|
||||
deactivated: 'Desativado',
|
||||
release_notes: 'Notas de Lançamento',
|
||||
activate_extension_details:
|
||||
'Torne a extensão disponível/indisponível para usuários',
|
||||
featured: 'Destacado',
|
||||
all: 'Todos',
|
||||
only_admins_can_install:
|
||||
'Apenas contas de administrador podem instalar extensões.',
|
||||
admin_only: 'Apenas para administradores',
|
||||
new_version: 'Nova Versão',
|
||||
extension_depends_on: 'Depende de:',
|
||||
extension_rating_soon: 'Avaliações em breve',
|
||||
extension_installed_version: 'Versão instalada',
|
||||
extension_uninstall_warning:
|
||||
'Você está prestes a remover a extensão para todos os usuários.',
|
||||
uninstall_confirm: 'Sim, Desinstalar',
|
||||
extension_db_drop_info:
|
||||
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
||||
extension_db_drop_warning:
|
||||
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
||||
extension_min_lnbits_version: 'Esta versão requer pelo menos a versão LNbits',
|
||||
payment_hash: 'Hash de pagamento',
|
||||
fee: 'Taxa',
|
||||
amount: 'Quantidade',
|
||||
tag: 'Etiqueta',
|
||||
unit: 'Unidade',
|
||||
description: 'Descrição',
|
||||
expiry: 'Validade',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Comprovativo de pagamento'
|
||||
payment_proof: 'Comprovativo de pagamento',
|
||||
update_available: 'Atualização %{version} disponível!',
|
||||
latest_update: 'Você está na última versão %{version}.',
|
||||
notifications: 'Notificações',
|
||||
no_notifications: 'Sem notificações',
|
||||
notifications_disabled:
|
||||
'As notificações de status do LNbits estão desativadas.',
|
||||
enable_notifications: 'Ativar Notificações',
|
||||
enable_notifications_desc:
|
||||
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
|
||||
enable_killswitch: 'Ativar Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Se ativado, ele mudará sua fonte de financiamento para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
|
||||
killswitch_interval: 'Intervalo do Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNBits proveniente da fonte de status (em minutos).',
|
||||
enable_watchdog: 'Ativar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se ativado, mudará automaticamente a sua fonte de financiamento para VoidWallet caso o seu saldo seja inferior ao saldo LNbits. Você precisará ativar manualmente após uma atualização.',
|
||||
watchdog_interval: 'Intervalo do Watchdog',
|
||||
watchdog_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar um sinal de desligamento no delta do watchdog [node_balance - lnbits_balance] (em minutos).',
|
||||
watchdog_delta: 'Observador Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limite antes que o killswitch altere a fonte de financiamento para VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Estado',
|
||||
notification_source: 'Fonte de Notificação',
|
||||
notification_source_label:
|
||||
'URL de Origem (use apenas a fonte oficial de status do LNbits e fontes em que confia)',
|
||||
more: 'mais',
|
||||
less: 'menos',
|
||||
releases: 'Lançamentos',
|
||||
killswitch: 'Interruptor de desativação',
|
||||
watchdog: 'Cão de guarda',
|
||||
server_logs: 'Registros do Servidor',
|
||||
ip_blocker: 'Bloqueador de IP',
|
||||
security: 'Segurança',
|
||||
security_tools: 'Ferramentas de segurança',
|
||||
block_access_hint: 'Bloquear acesso por IP',
|
||||
allow_access_hint: 'Permitir acesso por IP (substituirá IPs bloqueados)',
|
||||
enter_ip: 'Digite o IP e pressione enter.',
|
||||
rate_limiter: 'Limitador de Taxa',
|
||||
wallet_limiter: 'Limitador de Carteira',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Limite diário máximo de saque da carteira em sats (0 para desativar)',
|
||||
wallet_max_ballance: 'Saldo máximo da carteira em sats (0 para desativar)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minutos seg. entre transações por carteira (0 para desativar)',
|
||||
number_of_requests: 'Número de solicitações',
|
||||
time_unit: 'Unidade de tempo',
|
||||
minute: 'minuto',
|
||||
second: 'segundo',
|
||||
hour: 'hora',
|
||||
disable_server_log: 'Desativar Log do Servidor',
|
||||
enable_server_log: 'Ativar Log do Servidor',
|
||||
coming_soon: 'Funcionalidade em breve',
|
||||
session_has_expired: 'Sua sessão expirou. Por favor, faça login novamente.',
|
||||
instant_access_question: 'Quer acesso imediato?',
|
||||
login_with_user_id: 'Entrar com ID do usuário',
|
||||
or: 'ou',
|
||||
create_new_wallet: 'Criar Nova Carteira',
|
||||
login_to_account: 'Faça login na sua conta',
|
||||
create_account: 'Criar conta',
|
||||
account_settings: 'Configurações da Conta',
|
||||
signin_with_google: 'Entrar com o Google',
|
||||
signin_with_github: 'Entrar com o GitHub',
|
||||
signin_with_keycloak: 'Entrar com o Keycloak',
|
||||
username_or_email: 'Nome de usuário ou Email',
|
||||
password: 'Senha',
|
||||
password_config: 'Configuração de Senha',
|
||||
password_repeat: 'Repetição de senha',
|
||||
change_password: 'Alterar Senha',
|
||||
set_password: 'Definir Senha',
|
||||
invalid_password: 'A senha deve ter pelo menos 8 caracteres',
|
||||
login: 'Entrar',
|
||||
register: 'Registrar',
|
||||
username: 'Nome de usuário',
|
||||
user_id: 'ID do Usuário',
|
||||
email: 'E-mail',
|
||||
first_name: 'Nome próprio',
|
||||
last_name: 'Sobrenome',
|
||||
picture: 'Foto',
|
||||
verify_email: 'Verifique o e-mail com',
|
||||
account: 'Conta',
|
||||
update_account: 'Atualizar Conta',
|
||||
invalid_username: 'Nome de usuário inválido',
|
||||
auth_provider: 'Provedor de Autenticação',
|
||||
my_account: 'Minha Conta',
|
||||
back: 'Voltar',
|
||||
logout: 'Sair',
|
||||
look_and_feel: 'Aparência e Sensação',
|
||||
language: 'Idioma',
|
||||
color_scheme: 'Esquema de Cores'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
window.localisation.sk = {
|
||||
confirm: 'Áno',
|
||||
server: 'Server',
|
||||
theme: 'Téma',
|
||||
funding: 'Financovanie',
|
||||
users: 'Používatelia',
|
||||
apps: 'Aplikácie',
|
||||
channels: 'Kanály',
|
||||
transactions: 'Transakcie',
|
||||
dashboard: 'Prehľad',
|
||||
node: 'Uzol',
|
||||
total_capacity: 'Celková kapacita',
|
||||
avg_channel_size: 'Priemerná veľkosť kanálu',
|
||||
biggest_channel_size: 'Najväčší kanál',
|
||||
smallest_channel_size: 'Najmenší kanál',
|
||||
number_of_channels: 'Počet kanálov',
|
||||
active_channels: 'Aktívne kanály',
|
||||
connect_peer: 'Pripojiť peer',
|
||||
connect: 'Pripojiť',
|
||||
open_channel: 'Otvoriť kanál',
|
||||
open: 'Otvoriť',
|
||||
close_channel: 'Zatvoriť kanál',
|
||||
close: 'Zatvoriť',
|
||||
restart: 'Reštartovať server',
|
||||
save: 'Uložiť',
|
||||
save_tooltip: 'Uložiť vaše zmeny',
|
||||
topup: 'Doplniť',
|
||||
topup_wallet: 'Doplniť peňaženku',
|
||||
topup_hint: 'Použite ID peňaženky na doplnenie ľubovoľnej peňaženky',
|
||||
restart_tooltip: 'Pre prejavenie zmien reštartujte server',
|
||||
add_funds_tooltip: 'Pridať prostriedky do peňaženky.',
|
||||
reset_defaults: 'Obnoviť predvolené',
|
||||
reset_defaults_tooltip: 'Odstrániť všetky nastavenia a obnoviť predvolené.',
|
||||
download_backup: 'Stiahnuť zálohu databázy',
|
||||
name_your_wallet: 'Pomenujte vašu %{name} peňaženku',
|
||||
paste_invoice_label: 'Vložte faktúru, platobnú požiadavku alebo lnurl kód *',
|
||||
lnbits_description:
|
||||
'Ľahko nastaviteľný a ľahkotonážny, LNbits môže bežať na akomkoľvek zdroji financovania lightning-network, momentálne podporuje LND, Core Lightning, OpenNode, Alby, ZBD, LNPay a dokonca LNbits samotný! LNbits môžete používať pre seba, alebo ľahko ponúknuť správcovské riešenie pre iných. Každá peňaženka má svoje vlastné API kľúče a nie je limit na počet peňaženiek, ktoré môžete vytvoriť. Schopnosť rozdeľovať finančné prostriedky robí z LNbits užitočný nástroj pre správu peňazí a ako vývojový nástroj. Rozšírenia pridávajú extra funkčnosť do LNbits, takže môžete experimentovať s radou najnovších technológií na lightning sieti. Vývoj rozšírení sme urobili čo najjednoduchší a ako voľný a open-source projekt, podporujeme ľudí vývoj a odovzdávanie vlastných rozšírení.',
|
||||
export_to_phone: 'Exportovať do telefónu s QR kódom',
|
||||
export_to_phone_desc:
|
||||
'Tento QR kód obsahuje URL vašej peňaženky s plným prístupom. Môžete ho naskenovať z vášho telefónu a otvoriť vašu peňaženku odtiaľ.',
|
||||
wallets: 'Peňaženky',
|
||||
add_wallet: 'Pridať novú peňaženku',
|
||||
delete_wallet: 'Zmazať peňaženku',
|
||||
delete_wallet_desc:
|
||||
'Celá peňaženka bude zmazaná, prostriedky budú NEOBNOVITEĽNÉ.',
|
||||
rename_wallet: 'Premenovať peňaženku',
|
||||
update_name: 'Aktualizovať meno',
|
||||
fiat_tracking: 'Sledovanie fiat',
|
||||
currency: 'Mena',
|
||||
update_currency: 'Aktualizovať menu',
|
||||
press_to_claim: 'Stlačte pre nárok na bitcoin',
|
||||
donate: 'Prispieť',
|
||||
view_github: 'Zobraziť na GitHube',
|
||||
voidwallet_active: 'VoidWallet je aktívny! Platby zakázané',
|
||||
use_with_caution: 'POUŽÍVAJTE OPATRNE - %{name} peňaženka je stále v BETE',
|
||||
service_fee: 'Servisný poplatok: %{amount} % za transakciu',
|
||||
service_fee_max:
|
||||
'Servisný poplatok: %{amount} % za transakciu (max %{max} satoshi)',
|
||||
service_fee_tooltip:
|
||||
'Servisný poplatok účtovaný správcom LNbits servera za odchádzajúcu transakciu',
|
||||
toggle_darkmode: 'Prepnúť Tmavý režim',
|
||||
payment_reactions: 'Reakcie na platbu',
|
||||
view_swagger_docs: 'Zobraziť LNbits Swagger API dokumentáciu',
|
||||
api_docs: 'API dokumentácia',
|
||||
api_keys_api_docs: 'Adresa uzla, API kľúče a API dokumentácia',
|
||||
lnbits_version: 'Verzia LNbits',
|
||||
runs_on: 'Beží na',
|
||||
credit_hint: 'Stlačte Enter pre pripísanie na účet',
|
||||
credit_label: '%{denomination} na pripísanie',
|
||||
paste: 'Vložiť',
|
||||
paste_from_clipboard: 'Vložiť zo schránky',
|
||||
paste_request: 'Vložiť požiadavku',
|
||||
create_invoice: 'Vytvoriť faktúru',
|
||||
camera_tooltip: 'Použite kameru na naskenovanie faktúry/QR',
|
||||
export_csv: 'Exportovať do CSV',
|
||||
chart_tooltip: 'Zobraziť graf',
|
||||
pending: 'Čakajúce',
|
||||
copy_invoice: 'Kopírovať faktúru',
|
||||
withdraw_from: 'Vybrať z',
|
||||
cancel: 'Zrušiť',
|
||||
scan: 'Skenovať',
|
||||
read: 'Čítať',
|
||||
pay: 'Platiť',
|
||||
memo: 'Poznámka',
|
||||
date: 'Dátum',
|
||||
processing_payment: 'Spracovávanie platby...',
|
||||
not_enough_funds: 'Nedostatok prostriedkov!',
|
||||
search_by_tag_memo_amount: 'Vyhľadať podľa značky, poznámky, sumy',
|
||||
invoice_waiting: 'Faktúra čakajúca na zaplatenie',
|
||||
payment_received: 'Platba prijatá',
|
||||
payment_sent: 'Platba odoslaná',
|
||||
receive: 'prijímať',
|
||||
send: 'posielať',
|
||||
outgoing_payment_pending: 'Odchádzajúca platba čaká',
|
||||
drain_funds: 'Vyprázdniť prostriedky',
|
||||
drain_funds_desc:
|
||||
'Toto je LNURL-withdraw QR kód pre vyprázdnienie všetkého z tejto peňaženky. S nikým ho nezdieľajte. Je kompatibilný s balanceCheck a balanceNotify, takže vaša peňaženka môže naďalej kontinuálne vyťahovať prostriedky odtiaľto po prvom výbere.',
|
||||
i_understand: 'Rozumiem',
|
||||
copy_wallet_url: 'Kopírovať URL peňaženky',
|
||||
disclaimer_dialog:
|
||||
'Funkcionalita prihlásenia bude vydaná v budúcej aktualizácii, zatiaľ si uistite, že ste si túto stránku pridali medzi záložky pre budúci prístup k vašej peňaženke! Táto služba je v BETA verzii a nenesieme zodpovednosť za stratu prístupu k prostriedkom.',
|
||||
no_transactions: 'Zatiaľ žiadne transakcie',
|
||||
manage: 'Spravovať',
|
||||
extensions: 'Rozšírenia',
|
||||
no_extensions: 'Nemáte nainštalované žiadne rozšírenia :(',
|
||||
created: 'Vytvorené',
|
||||
search_extensions: 'Hľadať rozšírenia',
|
||||
warning: 'Upozornenie',
|
||||
repository: 'Repozitár',
|
||||
confirm_continue: 'Ste si istí, že chcete pokračovať?',
|
||||
manage_extension_details: 'Inštalovať/odinštalovať rozšírenie',
|
||||
install: 'Inštalovať',
|
||||
uninstall: 'Odinštalovať',
|
||||
drop_db: 'Odstrániť údaje',
|
||||
enable: 'Povoliť',
|
||||
enable_extension_details: 'Povoliť rozšírenie pre aktuálneho používateľa',
|
||||
disable: 'Zakázať',
|
||||
installed: 'Nainštalované',
|
||||
activated: 'Aktivované',
|
||||
deactivated: 'Deaktivované',
|
||||
release_notes: 'Poznámky k vydaniu',
|
||||
activate_extension_details:
|
||||
'Sprístupniť/neprístupniť rozšírenie pre používateľov',
|
||||
featured: 'Odporúčané',
|
||||
all: 'Všetky',
|
||||
only_admins_can_install:
|
||||
'(Iba administrátorské účty môžu inštalovať rozšírenia)',
|
||||
admin_only: 'Iba pre administrátorov',
|
||||
new_version: 'Nová verzia',
|
||||
extension_depends_on: 'Závisí na:',
|
||||
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
|
||||
extension_installed_version: 'Nainštalovaná verzia',
|
||||
extension_uninstall_warning:
|
||||
'Chystáte sa odstrániť rozšírenie pre všetkých používateľov.',
|
||||
uninstall_confirm: 'Áno, Odinštalovať',
|
||||
extension_db_drop_info:
|
||||
'Všetky údaje pre rozšírenie budú trvalo vymazané. Túto operáciu nie je možné vrátiť!',
|
||||
extension_db_drop_warning:
|
||||
'Chystáte sa odstrániť všetky údaje pre rozšírenie. Pre pokračovanie prosím napíšte názov rozšírenia:',
|
||||
extension_min_lnbits_version: 'Toto vydanie vyžaduje aspoň verziu LNbits',
|
||||
payment_hash: 'Hash platby',
|
||||
fee: 'Poplatok',
|
||||
amount: 'Suma',
|
||||
tag: 'Tag',
|
||||
unit: 'Jednotka',
|
||||
description: 'Popis',
|
||||
expiry: 'Expirácia',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Dôkaz platby',
|
||||
update_available: 'Dostupná aktualizácia %{version}!',
|
||||
latest_update: 'Máte najnovšiu verziu %{version}.',
|
||||
notifications: 'Notifikácie',
|
||||
no_notifications: 'Žiadne notifikácie',
|
||||
notifications_disabled: 'Notifikácie stavu LNbits sú zakázané.',
|
||||
enable_notifications: 'Povoliť Notifikácie',
|
||||
enable_notifications_desc:
|
||||
'Ak povolené, budú sa načítavať najnovšie aktualizácie stavu LNbits, ako sú bezpečnostné incidenty a aktualizácie.',
|
||||
enable_killswitch: 'Povoliť Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Ak povolené, vaš zdroj financovania sa automaticky zmení na VoidWallet, ak LNbits vysielajú signál killswitch. Po aktualizácii bude treba povoliť manuálne.',
|
||||
killswitch_interval: 'Interval Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Ako často by malo pozadie kontrolovať signál killswitch od LNbits zo zdroja stavu (v minútach).',
|
||||
enable_watchdog: 'Povoliť Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Ak povolené, vaš zdroj financovania sa automaticky zmení na VoidWallet, ak je váš zostatok nižší ako zostatok LNbits. Po aktualizácii bude treba povoliť manuálne.',
|
||||
watchdog_interval: 'Interval Watchdog',
|
||||
watchdog_interval_desc:
|
||||
'Ako často by malo pozadie kontrolovať signál killswitch v watchdog delta [node_balance - lnbits_balance] (v minútach).',
|
||||
watchdog_delta: 'Delta Watchdog',
|
||||
watchdog_delta_desc:
|
||||
'Limit pred zmenou zdroja financovania na VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Stav',
|
||||
notification_source: 'Zdroj notifikácií',
|
||||
notification_source_label:
|
||||
'URL zdroja (používajte len oficiálny LNbits zdroj stavu a zdroje, ktorým môžete dôverovať)',
|
||||
more: 'viac',
|
||||
less: 'menej',
|
||||
releases: 'Vydania',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Logy servera',
|
||||
ip_blocker: 'Blokovanie IP',
|
||||
security: 'Bezpečnosť',
|
||||
security_tools: 'Nástroje bezpečnosti',
|
||||
block_access_hint: 'Blokovať prístup podľa IP',
|
||||
allow_access_hint: 'Povoliť prístup podľa IP (prebije blokované IP)',
|
||||
enter_ip: 'Zadajte IP a stlačte enter',
|
||||
rate_limiter: 'Obmedzovač počtu požiadaviek',
|
||||
wallet_limiter: 'Obmedzovač peňaženky',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximálny denný výber z peňaženky v satošiach (0 pre zrušenie)',
|
||||
wallet_max_ballance:
|
||||
'Maximálny zostatok v peňaženke v satošiach (0 pre deaktiváciu)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minimálny počet sekúnd medzi transakciami na peňaženku (0 na deaktiváciu)',
|
||||
number_of_requests: 'Počet požiadaviek',
|
||||
time_unit: 'Časová jednotka',
|
||||
minute: 'minúta',
|
||||
second: 'sekunda',
|
||||
hour: 'hodina',
|
||||
disable_server_log: 'Zakázať Log servera',
|
||||
enable_server_log: 'Povoliť Log servera',
|
||||
coming_soon: 'Funkcia bude čoskoro dostupná',
|
||||
session_has_expired: 'Vaša relácia vypršala. Prosím, prihláste sa znova.',
|
||||
instant_access_question: 'Chcete okamžitý prístup?',
|
||||
login_with_user_id: 'Prihlásiť sa s používateľským ID',
|
||||
or: 'alebo',
|
||||
create_new_wallet: 'Vytvoriť novú peňaženku',
|
||||
login_to_account: 'Prihláste sa do vášho účtu',
|
||||
create_account: 'Vytvoriť účet',
|
||||
account_settings: 'Nastavenia účtu',
|
||||
signin_with_google: 'Prihlásiť sa pomocou Google',
|
||||
signin_with_github: 'Prihlásiť sa pomocou GitHub',
|
||||
signin_with_keycloak: 'Prihlásiť sa pomocou Keycloak',
|
||||
username_or_email: 'Používateľské meno alebo email',
|
||||
password: 'Heslo',
|
||||
password_config: 'Konfigurácia hesla',
|
||||
password_repeat: 'Opakovanie hesla',
|
||||
change_password: 'Zmeniť heslo',
|
||||
set_password: 'Nastaviť heslo',
|
||||
invalid_password: 'Heslo musí mať aspoň 8 znakov',
|
||||
login: 'Prihlásenie',
|
||||
register: 'Registrovať',
|
||||
username: 'Používateľské meno',
|
||||
user_id: 'ID používateľa',
|
||||
email: 'Email',
|
||||
first_name: 'Meno',
|
||||
last_name: 'Priezvisko',
|
||||
picture: 'Obrázok',
|
||||
verify_email: 'Overiť e-mail s',
|
||||
account: 'Účet',
|
||||
update_account: 'Aktualizovať účet',
|
||||
invalid_username: 'Neplatné užívateľské meno',
|
||||
auth_provider: 'Poskytovateľ autentifikácie',
|
||||
my_account: 'Môj účet',
|
||||
back: 'Späť',
|
||||
logout: 'Odhlásiť sa',
|
||||
look_and_feel: 'Vzhľad a dojem',
|
||||
language: 'Jazyk',
|
||||
color_scheme: 'Farebná schéma'
|
||||
}
|
||||
+168
-14
@@ -1,8 +1,26 @@
|
||||
window.localisation.we = {
|
||||
confirm: 'Ydw',
|
||||
server: 'Gweinydd',
|
||||
theme: 'Thema',
|
||||
funding: 'Arian fyndio',
|
||||
users: 'Defnyddwyr',
|
||||
apps: 'Apiau',
|
||||
channels: 'Sianelau',
|
||||
transactions: 'Trafodion',
|
||||
dashboard: 'Panel Gweinyddol',
|
||||
node: 'Nod',
|
||||
total_capacity: 'Capasiti Cyfanswm',
|
||||
avg_channel_size: 'Maint Sianel Cyf.',
|
||||
biggest_channel_size: 'Maint Sianel Fwyaf',
|
||||
smallest_channel_size: 'Maint Sianel Lleiaf',
|
||||
number_of_channels: 'Nifer y Sianeli',
|
||||
active_channels: 'Sianeli Gweithredol',
|
||||
connect_peer: 'Cysylltu â Chymar',
|
||||
connect: 'Cysylltu',
|
||||
open_channel: 'Sianel Agored',
|
||||
open: 'Agor',
|
||||
close_channel: 'Cau Sianel',
|
||||
close: 'cau',
|
||||
restart: 'Ailgychwyn gweinydd',
|
||||
save: 'Save',
|
||||
save_tooltip: 'cadw eich newidiadau',
|
||||
@@ -17,43 +35,54 @@ window.localisation.we = {
|
||||
name_your_wallet: 'Enwch eich waled %{name}',
|
||||
paste_invoice_label: 'Gludwch anfoneb, cais am daliad neu god lnurl *',
|
||||
lnbits_description:
|
||||
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt, ar hyn o bryd yn cefnogi LND, Core Lightning, OpenNode, LNPay a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
|
||||
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt, ar hyn o bryd yn cefnogi LND, Core Lightning, OpenNode, Alby, ZBD, LNPay a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
|
||||
export_to_phone: 'Allforio i Ffôn gyda chod QR',
|
||||
export_to_phone_desc:
|
||||
'Mae`r cod QR hwn yn cynnwys URL eich waled gyda mynediad llawn. Gallwch ei sganio o`ch ffôn i agor eich waled oddi yno.',
|
||||
waledi: 'Waledi',
|
||||
wallets: 'Waledi',
|
||||
add_wallet: 'Ychwanegu waled newydd',
|
||||
delete_wallet: 'Dileu waled',
|
||||
delete_wallet_desc:
|
||||
'Bydd y waled gyfan hon yn cael ei dileu, ni fydd modd adennill yr arian.',
|
||||
rename_wallet: 'Ailenwi waled',
|
||||
update_name: 'Diweddaru enw',
|
||||
fiat_tracking: 'Olrhain Fiat',
|
||||
currency: 'Arian Cyfred',
|
||||
update_currency: 'Diweddaru arian cyfred',
|
||||
press_to_claim: 'Pwyswch i hawlio bitcoin',
|
||||
Donate: 'Rhoi',
|
||||
donate: 'Rhoi',
|
||||
view_github: 'Gweld ar GitHub',
|
||||
voidwallet_active:
|
||||
' Mae VoidWallet yn weithredol! Taliadau wedi`u hanalluogi',
|
||||
use_with_caution: 'DEFNYDDIO GYDA GOFAL - mae waled %{name} yn dal yn BETA',
|
||||
service_fee: 'Ffi gwasanaeth: %{amount} % y trafodiad',
|
||||
service_fee_max:
|
||||
'Ffi gwasanaeth: %{amount} % y trafodiad (uchafswm %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
"Ffi gwasanaeth a godir gan weinyddwr gweinydd LNbits ym mhob trafodiad sy'n mynd allan",
|
||||
toggle_darkmode: 'Toglo Modd Tywyll',
|
||||
payment_reactions: 'Adweithiau Talu',
|
||||
view_swagger_docs: 'Gweld dogfennau API LNbits Swagger',
|
||||
api_docs: 'Api docs',
|
||||
api_docs: 'Dogfennau API',
|
||||
api_keys_api_docs: 'URL y nod, allweddi API a dogfennau API',
|
||||
lnbits_version: 'Fersiwn LNbits',
|
||||
Runs_on: 'Yn rhedeg ymlaen',
|
||||
runs_on: 'Yn rhedeg ymlaen',
|
||||
credit_hint: 'Pwyswch Enter i gyfrif credyd',
|
||||
credit_label: '%{enomination} i gredyd',
|
||||
credit_label: '%{denomination} i gredyd',
|
||||
paste: 'Gludo',
|
||||
paste_from_clipboard: "Gludo o'r clipfwrdd",
|
||||
paste_request: 'Gludo Cais',
|
||||
create_invoice: 'Creu Anfoneb',
|
||||
camera_tooltip: 'Defnyddio camera i sganio anfoneb/QR',
|
||||
export_csv: 'Allforio i CSV',
|
||||
trafodion: 'Trafodion',
|
||||
chart_tooltip: 'Dangos siart',
|
||||
pending: 'yn yr arfaeth',
|
||||
copy_invoice: 'Copi anfoneb',
|
||||
Close: 'cau',
|
||||
withdraw_from: 'Tynnu oddi ar',
|
||||
cancel: 'Canslo',
|
||||
scan: 'Sgan',
|
||||
read: 'Darllen',
|
||||
talu: 'Pay',
|
||||
pay: 'Talu',
|
||||
memo: 'Memo',
|
||||
date: 'Dyddiad',
|
||||
processing_payment: 'Prosesu taliad...',
|
||||
@@ -73,17 +102,142 @@ window.localisation.we = {
|
||||
disclaimer_dialog:
|
||||
'Swyddogaeth mewngofnodi i`w ryddhau mewn diweddariad yn y dyfodol, am y tro, gwnewch yn siŵr eich bod yn rhoi nod tudalen ar y dudalen hon ar gyfer mynediad i`ch waled yn y dyfodol! Mae`r gwasanaeth hwn yn BETA, ac nid ydym yn gyfrifol am bobl sy`n colli mynediad at arian.',
|
||||
no_transactions: 'Dim trafodion wedi`u gwneud eto',
|
||||
manage_extensions: 'Rheoli Estyniadau',
|
||||
manage_server: 'Rheoli Gweinydd',
|
||||
Extensions: 'Estyniadau',
|
||||
manage: 'Rheoli',
|
||||
extensions: 'Estyniadau',
|
||||
no_extensions: "Nid oes gennych unrhyw estyniadau wedi'u gosod :(",
|
||||
create: 'Crëwyd',
|
||||
created: 'Crëwyd',
|
||||
search_extensions: 'Chwilio estyniadau',
|
||||
warning: 'Rhybudd',
|
||||
repository: 'Ystorfa',
|
||||
confirm_continue: "Ydych chi'n siŵr eich bod chi eisiau parhau?",
|
||||
manage_extension_details: 'Gosod/dadosod estyniad',
|
||||
install: 'Gosod',
|
||||
uninstall: 'Dadgymhwyso',
|
||||
drop_db: 'Dileu Data',
|
||||
enable: 'Galluogi',
|
||||
enable_extension_details: 'Galluogi estyniad ar gyfer y defnyddiwr presennol',
|
||||
disable: 'Analluogi',
|
||||
installed: 'Gosodwyd',
|
||||
activated: "Wedi'i actifadu",
|
||||
deactivated: 'Anweithredol',
|
||||
release_notes: 'Nodiadau Rhyddhau',
|
||||
activate_extension_details: 'Gwneud estyniad ar gael/anar gael i ddefnyddwyr',
|
||||
featured: 'Nodweddwyd',
|
||||
all: 'Pob',
|
||||
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
|
||||
admin_only: 'Dim ond Gweinyddwr',
|
||||
new_version: 'Fersiwn Newydd',
|
||||
extension_depends_on: 'Dibynnu ar:',
|
||||
extension_rating_soon: 'Sgôr yn dod yn fuan',
|
||||
extension_installed_version: "Fersiwn wedi'i gosod",
|
||||
extension_uninstall_warning:
|
||||
"Rydych chi ar fin dileu'r estyniad ar gyfer pob defnyddiwr.",
|
||||
uninstall_confirm: 'Ie, Dad-osod',
|
||||
extension_db_drop_info:
|
||||
"Bydd yr holl ddata ar gyfer yr estyniad yn cael ei ddileu'n barhaol. Does dim ffordd o dadwneud y weithrediad hwn!",
|
||||
extension_db_drop_warning:
|
||||
"Rydych chi ar fin dileu'r holl ddata ar gyfer yr estyniad. Teipiwch enw'r estyniad i barhau:",
|
||||
extension_min_lnbits_version:
|
||||
"Mae'r rhyddhau hwn yn gofyn o leiaf am fersiwn LNbits",
|
||||
payment_hash: 'Hais Taliad',
|
||||
fee: 'Fee',
|
||||
amount: 'swm',
|
||||
tag: 'Tag',
|
||||
unit: 'Uned',
|
||||
description: 'Disgrifiad',
|
||||
expiry: 'dod i ben',
|
||||
webhook: 'bachyn we',
|
||||
payment_proof: 'prawf taliad'
|
||||
payment_proof: 'prawf taliad',
|
||||
update_available: 'Diweddariad %{version} ar gael!',
|
||||
latest_update: 'Rydych chi ar y fersiwn diweddaraf %{version}.',
|
||||
notifications: 'Hysbysiadau',
|
||||
no_notifications: 'Dim hysbysiadau',
|
||||
notifications_disabled: "Hysbysiadau statws LNbits wedi'u analluogi.",
|
||||
enable_notifications: 'Galluogi Hysbysiadau',
|
||||
enable_notifications_desc:
|
||||
"Os bydd wedi'i alluogi bydd yn nôl y diweddariadau Statws LNbits diweddaraf, fel digwyddiadau diogelwch a diweddariadau.",
|
||||
enable_killswitch: 'Galluogi Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Os bydd yn galluogi, bydd yn newid eich ffynhonnell arian i VoidWallet yn awtomatig os bydd LNbits yn anfon arwydd killswitch. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
|
||||
killswitch_interval: 'Amlder Cyllell Dorri',
|
||||
killswitch_interval_desc:
|
||||
"Pa mor aml y dylai'r dasg gefndir wirio am signal killswitch LNBits o'r ffynhonnell statws (mewn munudau).",
|
||||
enable_watchdog: 'Galluogi Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Os bydd yn cael ei alluogi bydd yn newid eich ffynhonnell ariannu i VoidWallet yn awtomatig os bydd eich balans yn is na balans LNbits. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
|
||||
watchdog_interval: 'Amserlennu Gwylio',
|
||||
watchdog_interval_desc:
|
||||
"Pa mor aml y dylai'r dasg gefndir wirio am signal torri yn y gwarchodfa delta [node_balance - lnbits_balance] (mewn munudau).",
|
||||
watchdog_delta: 'Watchdog Delta',
|
||||
watchdog_delta_desc:
|
||||
"Terfyn cyn i'r switshladd newid ffynhonnell ariannu i VoidWallet [lnbits_balance - node_balance > delta]",
|
||||
status: 'Statws',
|
||||
notification_source: 'Ffynhonnell Hysbysiad',
|
||||
notification_source_label:
|
||||
'URL Ffynhonnell (defnyddiwch yn unig ffynhonnell statws swyddogol LNbits, a ffynonellau y gallwch ymddiried ynddynt)',
|
||||
more: 'mwy',
|
||||
less: 'llai',
|
||||
releases: 'Rhyddhau',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Gwyliwr',
|
||||
server_logs: 'Logiau Gweinydd',
|
||||
ip_blocker: 'Rheolydd IP',
|
||||
security: 'Diogelwch',
|
||||
security_tools: 'Offer teclynnau diogelwch',
|
||||
block_access_hint: 'Atal mynediad gan IP',
|
||||
allow_access_hint:
|
||||
"Caniatáu mynediad gan IP (bydd yn diystyru IPs sydd wedi'u blocio)",
|
||||
enter_ip: 'Rhowch IP a gwasgwch enter',
|
||||
rate_limiter: 'Cyfyngydd Cyfradd',
|
||||
wallet_limiter: 'Cyfyngwr Waled',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Uchafswm tynnu’n ôl waled dyddiol mewn sats (0 i analluogi)',
|
||||
wallet_max_ballance: 'Uchafswm balans y waled mewn sats (0 i analluogi)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Eiliadau lleiaf rhwng trafodion fesul waled (0 i analluogi)',
|
||||
number_of_requests: 'Nifer y ceisiadau',
|
||||
time_unit: 'Uned amser',
|
||||
minute: 'munud',
|
||||
second: 'ail',
|
||||
hour: 'awr',
|
||||
disable_server_log: 'Analluogi Log Gweinydd',
|
||||
enable_server_log: 'Galluogi Log Gweinydd',
|
||||
coming_soon: 'Nodwedd yn dod yn fuan',
|
||||
session_has_expired: 'Mae eich sesiwn wedi dod i ben. Mewngofnodwch eto.',
|
||||
instant_access_question: 'Eisiau mynediad ar unwaith?',
|
||||
login_with_user_id: 'Mewngofnodi gyda ID y defnyddiwr',
|
||||
or: 'neu',
|
||||
create_new_wallet: 'Creu Waled Newydd',
|
||||
login_to_account: "Mewngofnodwch i'ch cyfrif",
|
||||
create_account: 'Creu cyfrif',
|
||||
account_settings: 'Gosodiadau Cyfrif',
|
||||
signin_with_google: 'Mewngofnodi gyda Google',
|
||||
signin_with_github: 'Mewngofnodi gyda GitHub',
|
||||
signin_with_keycloak: 'Mewngofnodi gyda Keycloak',
|
||||
username_or_email: 'Defnyddiwr neu E-bost',
|
||||
password: 'Cyfrinair',
|
||||
password_config: 'Ffurfweddiad Cyfrinair',
|
||||
password_repeat: 'Ailadrodd cyfrinair',
|
||||
change_password: 'Newid Cyfrinair',
|
||||
set_password: 'Gosod Cyfrinair',
|
||||
invalid_password: "Rhaid i'r cyfrinair gynnwys o leiaf 8 nod.",
|
||||
login: 'Mewngofnodi',
|
||||
register: 'Cofrestru',
|
||||
username: 'Enw defnyddiwr',
|
||||
user_id: 'ID Defnyddiwr',
|
||||
email: 'E-bost',
|
||||
first_name: 'Enw Cyntaf',
|
||||
last_name: 'Cyfenw',
|
||||
picture: 'Llun',
|
||||
verify_email: 'Gwirio e-bost gyda',
|
||||
account: 'Cyfrif',
|
||||
update_account: 'Diweddaru Cyfrif',
|
||||
invalid_username: 'Enw Defnyddiwr Annilys',
|
||||
auth_provider: 'Darparwr Dilysiad',
|
||||
my_account: 'Fy Nghyfrif',
|
||||
back: 'Yn ôl',
|
||||
logout: 'Allgofnodi',
|
||||
look_and_feel: 'Edrych a Theimlo',
|
||||
language: 'Iaith',
|
||||
color_scheme: 'Cynllun Lliw'
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user