Compare commits

...
159 Commits
Author SHA1 Message Date
dni ⚡andGitHub 5714848434 feat: install lnbits.sh bash script (#2684) (#3101) 2025-04-10 12:28:08 +02:00
dni ⚡andGitHub f87e5d4372 chore: update LNbits to version v1.0.0 (#3100) 2025-04-10 12:09:13 +02:00
Tiago VasconcelosandGitHub a0502f1676 [Fix] - Bug on switching wallets and payment details (#3094) 2025-04-10 10:02:35 +01:00
Vlad StanandGitHub f6c8a308dc feat: create new user from the command line (#3098) 2025-04-09 14:01:34 +02:00
dni ⚡andGitHub 6df3933c1b ci: trigger release on rc's > 9 (#3088) 2025-04-03 13:12:19 +02:00
dni ⚡andGitHub ffef085fa7 chore: update to v1.0.0-rc10 (#3087) 2025-04-03 12:55:30 +02:00
tlindiandGitHub 913991c72f Update fi.js-1.0.0-rc10-tobe (#3086) 2025-04-03 10:31:49 +03:00
dni ⚡andGitHub 6f611461f2 fix: FakeWallet, bolt11 payment_secret is not the preimage (#3084) 2025-04-02 18:46:24 +03:00
Tiago VasconcelosandGitHub 681385e2a2 fix dialog consistency (#3082) 2025-04-02 16:03:40 +02:00
dni ⚡andGitHub 883d52c303 fix: phoenixd uses sat instead of msat for pay_invoice (#3083) 2025-04-02 16:49:59 +03:00
Vlad StanandGitHub 1058461882 fix: include node balance in server status (#3079) 2025-04-02 15:31:47 +03:00
Vlad StanandGitHub 3a7a88857a fix: missing label (#3081) 2025-04-02 14:51:15 +03:00
Vlad StanandGitHub 6834b5e00f feat: do not allow regular admins to edit super user (#3077) 2025-04-01 14:01:02 +03:00
Vlad StanandGitHub 10093bb465 feat: show wallet payments on search (#3076) 2025-04-01 12:41:10 +02:00
dni ⚡andGitHub 1323a2005b feat: cleanup on library dir creation and upload endpoints (#3069) 2025-04-01 10:27:11 +03:00
Vlad StanandGitHub bafb4ddf75 fix: allow admin to view payments in deleted wallets (#3074) 2025-03-31 17:18:32 +03:00
Vlad StanandGitHub aa050eaf49 fix: make parallel calls for exchages (#3073) 2025-03-31 14:44:36 +03:00
ArcandGitHub 0c76efa2b9 Add ws link to curl examples (#3063) 2025-03-31 12:58:26 +03:00
PatMulliganandGitHub 681730a4af [CHORE] add responsive text scaling with clamp() for wallet balance display (#3071) 2025-03-31 11:59:58 +03:00
ArcandGitHub 63adcb6780 Adds image library for admins (#3066) 2025-03-27 11:16:20 +02:00
dni ⚡andGitHub 35f7821183 fix: make swagger understand the access token HTTPBearer (#3068) 2025-03-27 09:14:42 +00:00
Vlad StanandGitHub b185d9585c fix: add default extensions when a user is created (#3061) 2025-03-22 23:54:15 +00:00
ArcandGitHub 30a8d88ada Remove LNbits UI elements (#3064) 2025-03-22 21:08:26 +00:00
ArcandGitHub f7e984198b Revert "tweak: fix for ws payment" (#3060) 2025-03-21 14:03:52 +02:00
ArcandGitHub eb09e95e88 tweak: fix for ws payment (#3057) 2025-03-20 23:51:10 +00:00
Tiago VasconcelosandGitHub a883e2c7d8 hide 'Runs On' scroll if title is not LNbits (#3053) 2025-03-18 10:19:10 +01:00
Tiago VasconcelosandGitHub e06abb52ce [Fix] default to sats if wallet not fakewallet (#3055) 2025-03-18 10:17:47 +01:00
tlindiandGitHub 9f64b5349d Dev update fi.js again (#3052) 2025-03-18 11:07:37 +02:00
ArcandGitHub a6bbddce41 tweak: use image that works for fav and qr as default (#3051) 2025-03-14 10:48:58 +01:00
Tiago VasconcelosandGitHub adb9d24673 [Fix] Set default chart text color (#3048)
* fix: set default chart text
white when darkMode else black
2025-03-12 17:36:11 +01:00
Tiago VasconcelosandGitHub dc85f26964 fix: qr scanner button was missing the method (#3046) 2025-03-12 16:09:53 +01:00
Tiago VasconcelosandGitHub 14153b4a8f [Fix] Remove references and warnings for BETA (#3047)
* fix: remove BETA references/warnings
2025-03-12 16:09:18 +01:00
Vlad StanandGitHub b8a5f3942c [fix] limit the size of the invoice description to 640 characters (#3045) 2025-03-12 16:29:29 +02:00
dni ⚡andGitHub 475ae5736e chore: update to v1.0.0-rc9 (#3043) 2025-03-12 12:29:01 +01:00
PatMulliganandGitHub d7f0f4da0c create backwards compatibility for <=v1.0 (#3041)
in v0.12.12, the key is "payment_request" instead of the new "bolt11"
2025-03-12 12:27:24 +01:00
Tiago VasconcelosandGitHub f7b3444be4 fix: unfuck the swap mess (#3023) 2025-03-12 12:14:20 +01:00
ArcandGitHub 28e32a54a4 tweak: use qr code image for favicon as well (#3031)
use qr code image for favicon as well
2025-03-12 12:12:43 +01:00
ArcandGitHub 1833adc0a1 Tweak: moved custom denomination to fakewallet funding source (#3030) 2025-03-12 12:10:58 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dni ⚡
b759ec7468 chore(deps): bump axios from 1.7.7 to 1.8.2 (#3042)
* chore(deps): bump axios from 1.7.7 to 1.8.2

Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bundle

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2025-03-12 12:10:27 +01:00
2661d12ff1 tweak: fixes service fee badge (#3029)
* tweak: fixes service fee badge

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-03-12 12:08:08 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dni ⚡
ed419f27ac chore(deps): bump vue-i18n from 10.0.5 to 10.0.6 (#3022)
* chore(deps): bump vue-i18n from 10.0.5 to 10.0.6

Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 10.0.5 to 10.0.6.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v10.0.6/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bundle

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2025-03-12 12:01:18 +01:00
dni ⚡andGitHub c3efd48108 chore: use latest 1.X poetry version (#3039) 2025-03-11 09:54:36 +01:00
ArcandGitHub 4a28c22d79 tweak: fix 'test email' btn (#3026) 2025-03-10 18:37:11 +00:00
ArcandGitHub 0d0eb36de4 tweak: fix funding source hide (#3027)
added to title rather than all the inputs
2025-03-10 18:36:48 +00:00
ArcandGitHub 2aa73bfe75 tweak: punchy bitcoin theme (#3028) 2025-03-10 18:36:23 +00:00
Vlad StanandGitHub ebd080192c feat: return payment status in the websocket response (#3025) 2025-03-10 12:48:40 +02:00
Tiago VasconcelosandGitHub 41abe63f18 hide text on small screen (#3021) 2025-03-07 14:23:46 +00:00
dni ⚡andGitHub f61471b0f2 feat: add test email and improve on email notifications (#3018) 2025-03-05 09:53:40 +02:00
Vlad StanandGitHub 9bd037b6e7 [fix] settings cleanup (#3013) 2025-03-04 16:30:54 +02:00
a4ca88b6a4 tweak: dont show full id on wallet select box (#3017)
* tweak: dont show full id on wallet select box
* chore bundle

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-03-04 11:23:02 +01:00
ArcandGitHub f2b9aafc51 Tweak: Changed naming for password area (#3016) 2025-03-04 11:13:41 +01:00
Vlad StanandGitHub ec77a00f49 feat: only check the current page (#3015) 2025-03-03 16:28:55 +02:00
991e0db50b Feat: email notifications (#3007)
* Feat: email notifications / email db backup

Because not using a simple smtp connection is silly

* Added form

* broken, and annoying bug

Why wont the input display?!?!?

* make

* Encourage protonmail use, because its great.

* feat: small UI polishing

* chore: make bundle

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-03-03 12:59:08 +00:00
fe9b62e8a8 Fix: don't show register if new users not allowed (#3010)
* enforce allow new user flag on UI
* chore: bundle
* ensure we get a boolean

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-03-03 12:53:58 +01:00
dni ⚡andGitHub c5964436b5 fix: python versions were defined incorrectly and remove 3.9 (#3006)
* chore: update requirements to python3.10
* chore: format RUF007 rule for py310
* wrongly specified python version
python3.13 there is a greenlet build error
2025-03-03 12:53:30 +01:00
Vlad StanandGitHub 4ce14e2312 fix: rate lmitter init (#3011) 2025-02-28 12:50:50 +01:00
PatMulliganandGitHub 7021add193 Feat (crypto): Add URL-safe encoding option to AESCipher (#2984) 2025-02-28 11:17:53 +02:00
Tiago VasconcelosandGitHub 4be01a405c compare not assign (#3009)
fix missing = sign
2025-02-28 09:31:49 +02:00
Vlad StanandGitHub ebe9ff700f [fix] admin errors (#3004) 2025-02-27 11:00:15 +02:00
dni ⚡andGitHub a3619d40c6 fix: paying an invoice does not send a webhook. #2472 (#2999)
* fix: paying an invoice does not send a webhook. #2472
closes #2472
2025-02-27 09:23:49 +01:00
Vlad StanandGitHub 048aff3db4 [fix]: various issues discovered during testing (#3002)
* fix: prevent invalid calls to exchange APIs
* fix: audit conditions
* fix: audit filtering
2025-02-27 07:45:29 +01:00
Tiago VasconcelosandGitHub 89d85a0d7e [Fix] Denomination 3 characters (#3001)
* fix: denomination must be 3 chars ISO
2025-02-26 16:34:45 +01:00
5aa1f9b0f8 [feat] filter payments in wallet (#2997)
* fix: search payments
* fix rogue button
* feat: UI polishing
* feat: better charts

---------

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
2025-02-26 11:34:57 +01:00
Tiago VasconcelosandGitHub 5dc1705fa6 Fix a typo in reset password logic (#2998) 2025-02-25 13:52:03 +02:00
ArcandGitHub 0fc657c55c tweak: embit needs to be declared (#2996) 2025-02-24 15:05:20 +00:00
dni ⚡andGitHub 8641b34a3d chore: update lnbits version to 1.0.0-rc8 (#2995) 2025-02-24 14:53:52 +01:00
Vlad StanandGitHub 30cf6913af [feat]: invoice amount settings (#2990) 2025-02-24 14:45:17 +02:00
ArcandGitHub 10682cf736 tweak: checkout main not needed (#2993) 2025-02-24 12:26:42 +00:00
ArcandGitHub ee725846c5 tweak: just to make it obv port can be chnaged (#2992) 2025-02-24 12:19:42 +00:00
ArcandGitHub 69b31b1480 added libfuse2 to guide for fresh vps folks (#2991) 2025-02-24 12:15:29 +00:00
Tiago VasconcelosandGitHub 73a57ecf97 fix: payments filter (#2989) 2025-02-24 13:53:18 +02:00
dni ⚡andGitHub ab0f52d811 feat: add label for wallet fiat tracking (#2988) 2025-02-24 12:20:40 +01:00
dni ⚡andGitHub 8c59f3f4ae feat: add python-multipart for uploads (#2873) 2025-02-24 12:20:34 +02:00
ArcandGitHub 1bfb09ddab fix: default hide funding inputs (#2987)
* fix: default hide funding inputs

To protect admins from over the shoulder attackers, and the attackers from chipping away at their own morality.

* Remove need for function
2025-02-23 19:28:23 +00:00
ArcandGitHub 2af5ac9678 fix: wallet balance format (#2985) 2025-02-23 01:09:28 +00:00
4511891297 feat: modernize error page (#2949)
* can't figure this out!

* raise here so it gets picked up by exceptions

* add few more info for error rendering

* add a few more checks for browser

not fail safe just one more layer

* cleaner error display

... hopefully

* add go to extension

* keep buttons add go to extension

* feat: identify extensions that are not installed

* fix: status code

* fix: full path

* add account/logout button if 401

prevent getting stuck

* fix: ext access

* fix user button

* fix: 404 page

* fix: json 404 response

* fix: dumb rendering

* fix: `/api` request always json

* fix: extension api path

* test: check regtest

* test: investgate

* something made ws slower?

* fix: change error code

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2025-02-23 00:54:43 +00:00
eeca7de10d Fake wallet fixes (#2983)
* Fake wallet fixes

LNBITS_DENOMINATION and LNBITS_DENOMINATION == 'sats' everywhere!

* added exception to "/api/v1/rate/{currency}"

* fix: create invoice dialog

* cleaning expansion items in wallet

* Revert "fix: create invoice dialog"

This reverts commit 2b550392ba080cad708519fede897891e5b9e686.

* fix fakewallet mask break

* Fix formatted amount for fakewallet

* fixed wallet balance for fakewallet

* Wow, actually cleaning code, win!

* more cleaning

* For Vlad

* more cleaning

* make

* fix for sats

* bundle issue

* fix overflow in drawer for fat wallets and stupid long names

* make

* fixed adds

* fixed ad scale

* fundle

* fix

---------

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
2025-02-23 00:23:36 +00:00
b06c16ed57 fix: pay_invoice timeout to prevent blocking (#2875)
* fix: make invoice non blocking

* fix with vlad

* wallet.js take pending into account

* fixup!

* add check payment to outgoing

* revert

* bundle

* fix: label

* fix: rebase change

* feat: configure pay invoice wait time

* fix: check payment button

* bundle

* fix: task in async context

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-02-23 00:02:39 +00:00
dni ⚡andGitHub 2f2a3b1092 bug: settings fundingtab errors (#2982)
funding tab was not working if you had node ui enabled
2025-02-21 13:46:53 +02:00
Vlad StanandGitHub 9fe63b49d6 feat: add normalize_path helper (#2981) 2025-02-21 11:04:19 +02:00
Tiago VasconcelosandGitHub 6754ffb9dc change icon for pending (#2980) 2025-02-21 09:58:38 +02:00
Tiago VasconcelosandGitHub 4afe6dbd6d Change UI for Pay Invoice Dialog (#2979) 2025-02-18 18:42:46 +02:00
Tiago VasconcelosandGitHub 0cd94e654e adds a blur to the transparent bg (#2978) 2025-02-18 16:44:59 +02:00
Vlad StanandGitHub d1692a18dd fix: extract tag from extra (#2977) 2025-02-18 10:11:56 +02:00
Vlad Stan 0305d740ee feat: update LNBITS_SITE_TAGLINE 2025-02-18 09:35:28 +02:00
Tiago VasconcelosandGitHub 05777ca953 add blink logo (#2976) 2025-02-18 09:01:31 +02:00
Vlad StanandGitHub 1e083856ed fix: nostr login (#2975) 2025-02-17 16:18:04 +02:00
Vlad StanandGitHub c08623277e Chart polish (#2973) 2025-02-17 14:30:16 +02:00
Vlad StanandGitHub 60de308c8f fix: logout on safari (#2971) 2025-02-17 12:07:02 +02:00
ArcandGitHub 2aebe72df8 Quasarfy drawer scroll (#2965) 2025-02-16 02:00:33 +00:00
ArcandGitHub 2bd7600bcf Tweak so long wallet names/balances do not break styling (#2964) 2025-02-15 20:34:14 +00:00
tlindiandGitHub e318f7679e Update fi.js (#2963) 2025-02-14 19:44:08 +02:00
Vlad StanandGitHub 0912d7d1c8 fix: wallet url (#2961) 2025-02-13 22:54:28 +02:00
Vlad StanandGitHub bfa23568e3 [fix] callback url validation (#2959) 2025-02-13 15:11:46 +02:00
Vlad StanandGitHub b76d8b5458 fix: keep upgrades out of codebase (#2957)
* fix: keep upgrades out of codebase

* chore: code format
2025-02-12 13:31:53 +00:00
ArcandGitHub 1cef614349 Revert "Another small one (#2954)" (#2958)
This reverts commit 15ba3c33a3.
2025-02-12 12:06:09 +00:00
Vlad StanandGitHub 16fbfd1c6f Fix: date grouping for older SQLite versions (#2955) 2025-02-12 13:23:38 +02:00
Tiago VasconcelosandGitHub 15ba3c33a3 Another small one (#2954) 2025-02-12 10:56:22 +02:00
Tiago VasconcelosandGitHub b33a1903e8 check for g.user (#2953)
only render this if user is set
2025-02-11 17:25:42 +02:00
Vlad StanandGitHub 2d41a1bed3 Wallet polish (#2942) 2025-02-11 15:19:41 +02:00
ArcandGitHub 4d490506f3 Payments added to chart (#2952) 2025-02-11 12:26:04 +00:00
ArcandGitHub ed3a54e57a tweak: fun themes (#2951) 2025-02-10 23:20:30 +00:00
ArcandGitHub 61e8d0059b BG image/gradient to light fix (#2945) 2025-02-10 20:37:08 +00:00
ArcandGitHub 4a640dc123 Removes profile dropdown for public pages (#2943)
Global theme works for public pages.
2025-02-10 20:34:44 +00:00
ArcandGitHub 1f4160fda5 Adds extension#<ext name> option to extensions url (#2947) 2025-02-10 20:33:22 +00:00
ArcandGitHub dc91fbea9b Tweak: Updated Poetry install to include headers and make copy/paste easier (#2948)
* Tweak: Adds $USER to poetry install guide

Means a copy/paste will work for most

* auto agree for easy copy/paste

* poetry install and export

* ensure correct headers

* whoops

* typo
2025-02-10 20:32:33 +00:00
ArcandGitHub 7eb021f43c Adds no-border to fix the install card (#2950) 2025-02-10 20:31:41 +00:00
Vlad StanandGitHub 69350849b1 [fix] small fixes for ext upgrades (#2941) 2025-02-10 10:52:22 +02:00
Vlad StanandGitHub 828261d696 [fix]: small fixes v1 on demo DB (#2940) 2025-02-10 10:25:33 +02:00
ArcandGitHub 90dd3cc73a Fixes setting theme from account (#2939)
* getting there

* working

* make
2025-02-08 00:01:14 +00:00
ArcandGitHub 10c60291d2 white theme that doesnt hurt eyes (#2938) 2025-02-07 18:24:33 +00:00
ArcandGitHub 9d2e52f694 Fixes switching back to wallet (#2937)
* tals fix

* switched border back

* make

* bundle
2025-02-07 18:13:17 +00:00
Tiago VasconcelosandGitHub 503a599341 Fix login with oauth (#2935) 2025-02-07 16:48:22 +02:00
Vlad StanandGitHub a8809ed5a4 feat: migrate in chunks (#2936) 2025-02-07 14:16:53 +02:00
Tiago VasconcelosandGitHub e134c5c7b9 Fixes claiming LNURL (#2924) 2025-02-07 11:41:06 +02:00
ArcandGitHub 0efb52664e Feat: Creates .AppImage a̶n̶d̶ .d̶m̶g̶ on release (#2920) 2025-02-07 11:17:55 +02:00
Tiago VasconcelosandGitHub 53b00c6d41 UI improvements (#2926) 2025-02-07 10:09:15 +02:00
Tiago VasconcelosandGitHub 9a1d707f54 Revamp the UI on Login / Register page (#2919) 2025-02-07 09:43:19 +02:00
Tiago VasconcelosandGitHub 736699af94 . (#2925) 2025-02-06 15:10:16 +02:00
Tiago VasconcelosandGitHub 432b3a0fe0 Reset wallet keys (#2929) 2025-02-06 15:07:36 +02:00
Tiago VasconcelosandGitHub 34a959f0bc Add a payments page for admin (#2910) 2025-02-06 14:48:54 +02:00
Vlad Stan c1d26bb274 fix: email check 2025-02-06 11:57:01 +02:00
Vlad Stan b24a65cf2a doc: move security config at the top 2025-02-04 15:32:37 +02:00
ArcandGitHub 960c58db87 Feat: Adds wallet icon/color select (#2917) 2025-02-04 15:03:35 +02:00
ArcandGitHub dd9b217fdf Fix index.html error (#2923)
* Fix index.html error

* first install
2025-02-04 11:53:08 +00:00
Vlad StanandGitHub 7af9da4a58 fix: convert BigDecimal object to int (postgres only) (#2918) 2025-01-30 10:09:13 +02:00
ArcandGitHub c23eae9e08 Some menu naming changes (#2911) 2025-01-28 14:55:41 +02:00
ArcandGitHub 66b2ca2ac4 fixes wallet ws (#2912) 2025-01-28 14:14:12 +02:00
Tiago VasconcelosandGitHub 25e5423511 Add bar spinner (#2915) 2025-01-28 14:00:11 +02:00
Tiago VasconcelosandGitHub 4c7567a552 fix: router force update (#2914) 2025-01-28 13:44:37 +02:00
blackcoffeexbtandGitHub 73e716085b Translate expiry to expirySeconds for Phoenixd (#2901) 2025-01-28 11:54:33 +02:00
ArcandGitHub cc33a49b12 feature: Default theme for new users (#2898) 2025-01-28 11:29:38 +02:00
ArcandGitHub eeebaffbed feature: admin toggle to disable outgoing payments (#2909) 2025-01-28 10:27:24 +02:00
8633891485 fix: Add Breez SDK trampoline payments support (#2905)
* fix: add support for trampoline payments in Breez integration

- Introduced `BREEZ_USE_TRAMPOLINE` configuration in `.env.example`.
- Updated `BreezSdkFundingSource` to include `breez_use_trampoline` setting.
- Modified payment request in `breez.py` to utilize the trampoline setting when sending payments.

* Update .env.example

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>

* Update lnbits/settings.py

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>

* refactor: streamline payment request formatting in Breez integration

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2025-01-25 22:21:55 +01:00
Pavol RusnakandGitHub cb5cc1e5e8 make things work with Python 3.13 + fix Nix (#2908)
* update pyproject to make things work with python 3.13

* fix nix build
2025-01-23 23:11:45 +02:00
Vlad StanandGitHub f845bfe651 feat: do not allow user_id_only login for admins (#2904) 2025-01-23 15:01:54 +02:00
Vlad StanandGitHub b6bdf50ed7 [feat] Watchdog and notifications (#2895) 2025-01-23 13:23:09 +02:00
Vlad StanandGitHub 56a4b702f3 fix: multiple ws connections (#2907) 2025-01-23 13:05:35 +02:00
ArcandGitHub 273ab9781f fix: fiat tracking / updating wallet without reloads (#2891) 2025-01-22 10:27:05 +02:00
ArcandGitHub 8a759c8fc1 feature: Optional bg image and neon borders (#2897)
* init

* working
2025-01-18 19:27:50 +00:00
Tiago VasconcelosandGitHub c7ebe09463 Fix payment table refresh (#2894) 2025-01-17 09:49:14 +02:00
ArcandGitHub 5d4d22dcdd fix: for g.user error on public pages (#2892)
* fix for g.user error

* dont even mount
2025-01-16 16:18:15 +00:00
Vlad StanandGitHub b164317121 [feat] access control lists (with access tokens) (#2864) 2025-01-16 17:25:27 +02:00
Tiago VasconcelosandGitHub f415a92914 feat: harmonize the UI for payment dialog (#2882) 2025-01-16 14:31:33 +02:00
Vlad Stan 16c78f1901 fix: websocket prefix 2025-01-16 14:19:26 +02:00
ArcandGitHub 6a08d20fe8 v1 feat: Vue Routes (#2872) 2025-01-16 13:23:34 +02:00
dni ⚡andGitHub 5abfbdd07a fix: lnbits/upgrades where not ignore (#2889)
after updating extension i saw a dirty repo with the upgrades, this one
ignores them for sure
2025-01-15 18:32:53 +01:00
Riccardo BalboandGitHub bba7614012 fix: Add timeout to wait_for_connection (#2885) 2025-01-14 11:31:12 +02:00
dni ⚡andGitHub 2ee7180aa0 fix: cln nodemanager errors on failed transactions (#2865) 2025-01-13 17:58:37 +02:00
Pavol RusnakandGitHub 982732cbd4 i18n ai (#2881) 2025-01-11 12:53:52 +01:00
dfdb34852c chore: update i18n labels (#2871)
---------

Co-authored-by: dni  <office@dnilabs.com>
2025-01-10 12:15:51 +01:00
8fa5c81088 chore: update to breez-sdk 0.6.6 (#2878)
* chore: update to breez-sdk 0.6.6

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2025-01-10 11:56:10 +01:00
91748fc891 fix: restore user type badge (#2861)
---------

Co-authored-by: dni  <office@dnilabs.com>
2025-01-10 11:55:05 +01:00
Riccardo BalboandGitHub 0574837950 fix: NWC mark and remove subs before sending close request (#2870)
* mark and remove subscription even if close event fails to send

* catch CLOSE subscription exceptions

* format
2025-01-10 12:41:16 +02:00
dni ⚡andGitHub 7e27b2c6f0 fix: lndrest nodemanager get_channel (flaky ci decoy) (#2877) 2025-01-09 17:04:32 +01:00
6f3d0edfbd fix: ci with docker image built (downgrade poetry) (#2876)
* fix: ci with docker image built (downgrade poetry)

* use latest 1.8.x poetry

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2025-01-09 11:21:41 +02:00
dni ⚡andGitHub 3900d2871d feat: nodemanager, view and edit channels fees (#2818) 2024-12-20 09:48:09 +01:00
ArcandGitHub 0c5b909c7a Added settings links and link for theme (#2860) 2024-12-20 10:07:50 +02:00
ArcandGitHub 78437eaf94 feat: moved search inside the transaction table (#2859) 2024-12-20 07:44:07 +01:00
161 changed files with 17209 additions and 5389 deletions
+15 -12
View File
@@ -1,5 +1,17 @@
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
######################################
####### Auth Configurations ##########
######################################
# Secret Key: will default to the hash of the super user.
# !!!!! It is strongly recommended that you set your own strong random value !!!!
AUTH_SECRET_KEY=""
AUTH_TOKEN_EXPIRE_MINUTES=525600
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, 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"
######################################
########### Admin Settings ###########
######################################
@@ -14,7 +26,7 @@ LNBITS_ADMIN_UI=true
# Change theme
LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_TAGLINE="Open Source Lightning Payments Platform"
LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack."
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
@@ -133,17 +145,8 @@ BREEZ_GREENLIGHT_SEED=SEED
BREEZ_GREENLIGHT_INVITE_CODE=CODE
BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRING
BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
# BREEZ_USE_TRAMPOLINE=true
######################################
####### 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, nostr-auth-nip98, 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
@@ -183,7 +186,7 @@ LNBITS_ADMIN_USERS=""
# SUPER_USER=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
LNBITS_ADMIN_EXTENSIONS="ngrok, nostrclient"
# Extensions enabled by default when a user is created
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
+1 -1
View File
@@ -7,7 +7,7 @@ inputs:
default: "3.10"
poetry-version:
description: "Poetry Version"
default: "1.7.0"
default: "1.8.5"
node-version:
description: "Node Version"
default: "20.x"
+81
View File
@@ -0,0 +1,81 @@
#!/bin/sh
LAUNCH_DIR="$PWD"
# Define persistent storage for extracted LNbits
PERSISTENT_DIR="$HOME/.local/share/lnbits"
# Remove existing LNbits directory before extraction
if [ -d "$PERSISTENT_DIR" ]; then
echo "Removing existing LNbits directory..."
rm -rf "$PERSISTENT_DIR"
fi
# Ensure the persistent directory exists
mkdir -p "$PERSISTENT_DIR"
# Extract LNbits from the AppImage if not already extracted
echo "Extracting LNbits to disk for better performance..."
cp -r "$APPDIR/usr/lnbits"/* "$PERSISTENT_DIR/"
chmod +x "$PERSISTENT_DIR/dist/lnbits"
# Check if the directory exists, and create it if it doesn't
if [ ! -d "$LAUNCH_DIR/lnbits/database" ]; then
mkdir -p "$LAUNCH_DIR/lnbits/database"
echo "Created database directory at $LAUNCH_DIR/lnbits/database"
fi
if [ ! -d "$LAUNCH_DIR/lnbits/extensions" ]; then
mkdir -p "$LAUNCH_DIR/lnbits/extensions"
echo "Created extensions directory at $LAUNCH_DIR/lnbits/extensions"
fi
cd "$PERSISTENT_DIR"
# Export the directory as an environment variable for the app
LNBITS_DATA_FOLDER="${LNBITS_DATA_FOLDER:-$LAUNCH_DIR/lnbits/database}"
LNBITS_EXTENSIONS_PATH="${LNBITS_EXTENSIONS_PATH:-$LAUNCH_DIR/lnbits/extensions}"
export LNBITS_DATA_FOLDER
export LNBITS_EXTENSIONS_PATH
export LNBITS_ADMIN_UI=true
# Define the LNbits URL
URL="http://0.0.0.0:5000"
"./dist/lnbits" "$@" &
LNBITS_PID=$!
# Wait for LNbits to be ready before showing the popup
sleep 3
CLOSED=false
# Function to stop LNbits gracefully
kill_lnbits() {
LN_PIDS=$(lsof -t -i:5000 2>/dev/null) # Capture all PIDs
if [ -n "$LN_PIDS" ]; then
echo "Stopping LNbits (PIDs: $LN_PIDS)..."
kill -2 $LN_PIDS # Send SIGINT to all processes on port 5000
CLOSED=true
fi
}
# Show a GUI with a clickable link to open the browser
if command -v zenity >/dev/null 2>&1; then
while [ "$CLOSED" = false ]; do
zenity --info --title="LNbits" --width=400 --text="<b>LNbits is running.</b>\n\n<a href='$URL'>$URL</a>\n\nClick 'Close Server' to stop LNbits." --ok-label="Close Server"
kill_lnbits
sleep 1
done
elif command -v yad >/dev/null 2>&1; then
while [ "$CLOSED" = false ]; do
yad --title="LNbits" --width=400 --text="<b>LNbits is running.</b>\n\n<a href='$URL'>$URL</a>\n\nClick 'Close Server' to stop LNbits." --button="Close Server":0
kill_lnbits
sleep 1
done
else
echo "No GUI tool found. LNbits is running at $URL"
fi
# Ensure the script doesn't hang after closing
if ps -p $LNBITS_PID >/dev/null 2>&1; then
wait $LNBITS_PID 2>/dev/null || true
fi
@@ -0,0 +1,6 @@
[Desktop Entry]
Name=LNbits
Exec=lnbits
Icon=lnbits
Type=Application
Categories=X-Bitcoin;X-LightningNetwork;X-Finance;Network;
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+72
View File
@@ -0,0 +1,72 @@
name: Build LNbits AppImage DMG
on:
release:
types: [published]
jobs:
build-linux-package:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
# Step 2: Install Dependencies
- name: Install Dependencies
run: |
curl -sSL https://install.python-poetry.org | python3 -
echo "$HOME/.local/bin" >> $GITHUB_PATH
sudo apt-get update
sudo apt-get install -y libfuse2
shell: bash
# Step 3: Clone LNbits Repository
- name: Clone LNbits
run: |
mv .github/packaging packaging
mkdir -p packaging/linux/AppDir/usr
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
shell: bash
# Step 4: Make the AppImage Asset
- name: Make Asset
run: |
cd packaging/linux/AppDir/usr/lnbits
poetry install
poetry run pip install pyinstaller
# Build the LNbits binary
poetry run pyinstaller --onefile --name lnbits --hidden-import=embit --collect-all embit --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
cd ../../../../..
chmod +x packaging/linux/AppDir/AppRun
chmod +x packaging/linux/AppDir/lnbits.desktop
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
! -name 'dist' \
! -name 'lnbits' \
-exec rm -rf {} +
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
TAG_NAME=${{ github.event.release.tag_name }}
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
./appimagetool-x86_64.AppImage --updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" packaging/linux/AppDir "$APPIMAGE_NAME"
chmod +x "$APPIMAGE_NAME"
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
shell: bash
# Step 5: Upload Linux Release Asset
- name: Upload Linux Release Asset
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ${{ env.APPIMAGE_NAME }}
asset_name: ${{ env.APPIMAGE_NAME }}
asset_content_type: application/octet-stream
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2
View File
@@ -63,6 +63,8 @@ jobs:
LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
ECLAIR_PASS: lnbits
PYTHONUNBUFFERED: 1
DEBUG: true
+1 -1
View File
@@ -3,7 +3,7 @@ name: release-rc
on:
push:
tags:
- "*-rc[0-9]"
- "*-rc[0-9]+"
permissions:
contents: write
+3 -1
View File
@@ -39,6 +39,8 @@ lnbits/static/bundle-components.js
lnbits/static/bundle.css
lnbits/static/bundle.min.js.old
lnbits/static/bundle.min.css.old
lnbits/static/bundle-components.min.js.old
lnbits/upgrades
docker
# Nix
@@ -51,7 +53,7 @@ lnbits-backup.zip
# Ignore extensions (post installable extension PR)
/lnbits/extensions
/upgrades/
/lnbits/upgrades/
# builded python package
dist
+2 -2
View File
@@ -4,7 +4,7 @@ RUN apt-get clean
RUN apt-get update
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
@@ -32,7 +32,7 @@ RUN apt-get update && apt-get -y upgrade && \
apt-get -y install postgresql-client-14 postgresql-client-common && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
ENV PATH="/root/.local/bin:$PATH"
ENV POETRY_NO_INTERACTION=1 \
+26 -12
View File
@@ -6,30 +6,44 @@ nav_order: 2
# Basic installation
The following sections explain how to install LNbits using varions package managers: `poetry`, `nix`, `Docker` and `Fly.io`.
Note that by default LNbits uses SQLite as its database, which is simple and effective but you can configure it to use PostgreSQL instead which is also described in a section below.
## Option 1 (recommended): Poetry
## Option 1: AppImage (LInux)
### AppImage (Linux)
Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppImage, or:
```sh
sudo apt-get install libfuse2
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
chmod +x LNbits-latest.AppImage
./LNbits-latest.AppImage --host 0.0.0.0 --port 5000
```
LNbits will create a folder for db and extension files in the folder the AppImage runs from.
## Option 2: Poetry (recommended for developers)
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
### Install Python 3.12
## Option 2 (recommended): Poetry
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
### Verify Python version
```sh
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.12
sudo apt-get install python3.12-dev # ensure correct headers needed for secp256k1
python3 --version
```
### Install Poetry
```sh
curl -sSL https://install.python-poetry.org | python3 -
# Once the above poetry install is completed, use the installation path printed to terminal and replace in the following command
export PATH="/home/user/.local/bin:$PATH"
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.local/bin:$PATH"
```
### install LNbits
@@ -81,7 +95,7 @@ chmod +x lnbits.sh &&
Now visit `0.0.0.0:5000` to make a super-user account.
`export PATH="/home/$USER/.local/bin:$PATH"` then `./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
## Option 3: Nix
@@ -226,7 +240,7 @@ Problems installing? These commands have helped us install LNbits.
sudo apt install pkg-config libffi-dev libpq-dev
# build essentials for debian/ubuntu
sudo apt install python3.9-dev gcc build-essential
sudo apt install python3.10-dev gcc build-essential
# if the secp256k1 build fails:
# if you used poetry
Generated
+3 -3
View File
@@ -41,11 +41,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1723938990,
"narHash": "sha256-9tUadhnZQbWIiYVXH8ncfGXGvkNq3Hag4RCBEMUk7MI=",
"lastModified": 1735563628,
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c42fcfbdfeae23e68fc520f9182dde9f38ad1890",
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
"type": "github"
},
"original": {
+10
View File
@@ -34,6 +34,16 @@
protobuf = prev.protobuf.override { preferWheel = true; };
ruff = prev.ruff.override { preferWheel = true; };
wallycore = prev.wallycore.override { preferWheel = true; };
tlv8 = prev.tlv8.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
prev.setuptools
];
});
pynostr = prev.pynostr.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
prev.setuptools-scm
];
});
});
};
});
-3
View File
@@ -42,9 +42,6 @@ elif [ ! -d lnbits/wallets ]; then
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
fi
# Set path for running after install
export PATH="/home/$USER/.local/bin:$PATH"
# Install the dependencies using Poetry
poetry env use python3.9
poetry install --only main
+39 -16
View File
@@ -24,14 +24,17 @@ from lnbits.core.crud import (
)
from lnbits.core.crud.extensions import create_installed_extension
from lnbits.core.helpers import migrate_extension_database
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
from lnbits.core.tasks import ( # watchdog_task
from lnbits.core.services.notifications import enqueue_notification
from lnbits.core.tasks import (
audit_queue,
collect_exchange_rates_data,
killswitch_task,
purge_audit_data,
run_by_the_minute_tasks,
wait_for_audit_data,
wait_for_paid_invoices,
wait_notification_messages,
)
from lnbits.exceptions import register_exception_handlers
from lnbits.helpers import version_parse
@@ -78,6 +81,7 @@ async def startup(app: FastAPI):
# setup admin settings
await check_admin_settings()
core_app_extra.register_new_ratelimiter()
await check_webpush_settings()
# check extensions after restart
@@ -102,9 +106,24 @@ async def startup(app: FastAPI):
# initialize tasks
register_async_tasks()
enqueue_notification(
NotificationType.server_start_stop,
{
"message": "LNbits server started.",
"up_time": settings.lnbits_server_up_time,
},
)
async def shutdown():
logger.warning("LNbits shutting down...")
enqueue_notification(
NotificationType.server_start_stop,
{
"message": "LNbits server shutting down...",
"up_time": settings.lnbits_server_up_time,
},
)
settings.lnbits_running = False
# shutdown event
@@ -144,9 +163,13 @@ def create_app() -> FastAPI:
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
# register static files
static_path = Path("lnbits", "static")
static = StaticFiles(directory=static_path)
app.mount("/static", static, name="static")
app.mount("/static", StaticFiles(directory=Path("lnbits", "static")), name="static")
Path(settings.lnbits_data_folder, "images").mkdir(parents=True, exist_ok=True)
app.mount(
"/library",
StaticFiles(directory=Path(settings.lnbits_data_folder, "images")),
name="library",
)
g().base_url = f"http://{settings.host}:{settings.port}"
@@ -265,14 +288,14 @@ async def build_all_installed_extensions_list(
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
"""
installed_extensions = await get_installed_extensions()
settings.lnbits_all_extensions_ids = {e.id for e in installed_extensions}
settings.lnbits_installed_extensions_ids = {e.id for e in installed_extensions}
for ext_dir in Path(settings.lnbits_extensions_path, "extensions").iterdir():
try:
if not ext_dir.is_dir():
continue
ext_id = ext_dir.name
if ext_id in settings.lnbits_all_extensions_ids:
if ext_id in settings.lnbits_installed_extensions_ids:
continue
ext_info = InstallableExtension.from_ext_dir(ext_id)
if not ext_info:
@@ -287,7 +310,7 @@ async def build_all_installed_extensions_list(
logger.warning(e)
for ext_id in settings.lnbits_extensions_default_install:
if ext_id in settings.lnbits_all_extensions_ids:
if ext_id in settings.lnbits_installed_extensions_ids:
continue
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
@@ -348,6 +371,11 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
def register_custom_extensions_path():
upgrades_dir = settings.lnbits_extensions_upgrade_path
shutil.rmtree(upgrades_dir, True)
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
sys.path.append(str(upgrades_dir))
if settings.has_default_extension_path:
return
default_ext_path = os.path.join("lnbits", "extensions")
@@ -366,11 +394,6 @@ def register_custom_extensions_path():
Path(extensions_dir).mkdir(parents=True, exist_ok=True)
sys.path.append(str(extensions_dir))
upgrades_dir = Path(settings.lnbits_extensions_path, "upgrades")
shutil.rmtree(upgrades_dir, True)
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
sys.path.append(str(upgrades_dir))
def register_new_ext_routes(app: FastAPI) -> Callable:
# Returns a function that registers new routes for an extension.
@@ -444,6 +467,8 @@ async def check_and_register_extensions(app: FastAPI):
def register_async_tasks():
create_permanent_task(wait_for_audit_data)
create_permanent_task(wait_notification_messages)
create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
@@ -454,9 +479,7 @@ def register_async_tasks():
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)
create_permanent_task(run_by_the_minute_tasks)
create_permanent_task(purge_audit_data)
create_permanent_task(collect_exchange_rates_data)
+38 -15
View File
@@ -5,6 +5,7 @@ import time
from functools import wraps
from pathlib import Path
from typing import Optional
from uuid import uuid4
import click
import httpx
@@ -27,13 +28,13 @@ from lnbits.core.crud import (
update_payment,
)
from lnbits.core.helpers import is_valid_url, migrate_databases
from lnbits.core.models import Payment, PaymentState
from lnbits.core.models import Account, Payment, PaymentState
from lnbits.core.models.extensions import (
CreateExtension,
ExtensionRelease,
InstallableExtension,
)
from lnbits.core.services import check_admin_settings
from lnbits.core.services import check_admin_settings, create_user_account_no_ckeck
from lnbits.core.views.extension_api import (
api_install_extension,
api_uninstall_extension,
@@ -64,6 +65,13 @@ def db():
"""
@lnbits_cli.group()
def users():
"""
Users related commands
"""
@lnbits_cli.group()
def extensions():
"""
@@ -109,7 +117,7 @@ async def delete_settings():
"""Deletes the settings"""
async with core_db.connect() as conn:
await conn.execute("DELETE from settings")
await conn.execute("DELETE from system_settings")
@db.command("migrate")
@@ -180,17 +188,6 @@ async def database_revert_payment(checking_id: str):
click.echo(f"Payment '{checking_id}' marked as pending.")
@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)
@db.command("check-payments")
@click.option("-d", "--days", help="Maximum age of payments in days.")
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
@@ -271,6 +268,32 @@ async def check_invalid_payments(
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
@users.command("new")
@click.option("-u", "--username", required=True, help="Username.")
@click.option("-p", "--password", required=True, help="Password.")
@coro
async def create_user(username: str, password: str):
"""Create a new user bypassing the system 'new_accounts_allowed' rules"""
account = Account(
id=uuid4().hex,
username=username,
)
account.hash_password(password)
user = await create_user_account_no_ckeck(account)
click.echo(f"User '{user.username}' created. Id: '{user.id}'")
@users.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)
@extensions.command("list")
@coro
async def extensions_list():
@@ -339,7 +362,7 @@ async def extensions_update(
if not await _can_run_operation(url):
return
upgrades_dir = Path(settings.lnbits_extensions_path, "upgrades")
upgrades_dir = settings.lnbits_extensions_upgrade_path
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
sys.path.append(str(upgrades_dir))
+2
View File
@@ -58,6 +58,7 @@ from .users import (
get_account_by_username_or_email,
get_accounts,
get_user,
get_user_access_control_lists,
get_user_from_account,
update_account,
)
@@ -145,6 +146,7 @@ __all__ = [
"get_accounts",
"get_user",
"get_user_from_account",
"get_user_access_control_lists",
"update_account",
# wallets
"create_wallet",
+142 -20
View File
@@ -1,25 +1,23 @@
from time import time
from typing import Literal, Optional
from typing import Any, Optional, Tuple
from lnbits.core.crud.wallets import get_total_balance, get_wallet
from lnbits.core.db import db
from lnbits.core.models import PaymentState
from lnbits.db import DB_TYPE, SQLITE, Connection, Filters, Page
from lnbits.db import Connection, DateTrunc, Filters, Page
from ..models import (
CreatePayment,
Payment,
PaymentCountField,
PaymentCountStat,
PaymentDailyStats,
PaymentFilters,
PaymentHistoryPoint,
PaymentsStatusCount,
PaymentWalletStats,
)
DateTrunc = Literal["hour", "day", "month"]
sqlite_formats = {
"hour": "%Y-%m-%d %H:00:00",
"day": "%Y-%m-%d 00:00:00",
"month": "%Y-%m-01 00:00:00",
}
def update_payment_extra():
pass
@@ -87,7 +85,7 @@ async def get_latest_payments_by_extension(
WHERE status = '{PaymentState.SUCCESS}'
AND extra LIKE :ext_name
AND extra LIKE :ext_id
ORDER BY time DESC LIMIT {limit}
ORDER BY time DESC LIMIT {int(limit)}
""",
{"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"},
Payment,
@@ -99,6 +97,7 @@ async def get_payments_paginated(
wallet_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
failed: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
@@ -107,11 +106,11 @@ async def get_payments_paginated(
conn: Optional[Connection] = None,
) -> Page[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
Filters payments to be returned by:
- complete | pending | failed | outgoing | incoming.
"""
values: dict = {
"wallet_id": wallet_id,
values: dict[str, Any] = {
"time": since,
}
clause: list[str] = []
@@ -120,6 +119,7 @@ async def get_payments_paginated(
clause.append(f"time > {db.timestamp_placeholder('time')}")
if wallet_id:
values["wallet_id"] = wallet_id
clause.append("wallet_id = :wallet_id")
if complete and pending:
@@ -137,6 +137,8 @@ async def get_payments_paginated(
)
elif pending:
clause.append(f"status = '{PaymentState.PENDING}'")
elif failed:
clause.append(f"status = '{PaymentState.FAILED}'")
if outgoing and incoming:
pass
@@ -200,6 +202,21 @@ async def get_payments(
return page.data
async def get_payments_status_count() -> PaymentsStatusCount:
empty_page: Filters = Filters(limit=0)
in_payments = await get_payments_paginated(incoming=True, filters=empty_page)
out_payments = await get_payments_paginated(outgoing=True, filters=empty_page)
pending_payments = await get_payments_paginated(pending=True, filters=empty_page)
failed_payments = await get_payments_paginated(failed=True, filters=empty_page)
return PaymentsStatusCount(
incoming=in_payments.total,
outgoing=out_payments.total,
pending=pending_payments.total,
failed=failed_payments.total,
)
async def delete_expired_invoices(
conn: Optional[Connection] = None,
) -> None:
@@ -234,6 +251,7 @@ async def create_payment(
# note: this can be removed if the db uniqueness constraints are set appropriately
previous_payment = await get_standalone_payment(checking_id, conn=conn)
assert previous_payment is None, "Payment already exists"
extra = data.extra or {}
payment = Payment(
checking_id=checking_id,
@@ -247,7 +265,8 @@ async def create_payment(
expiry=data.expiry,
webhook=data.webhook,
fee=data.fee,
extra=data.extra or {},
tag=extra.get("tag", None),
extra=extra,
)
await (conn or db).insert("apipayments", payment)
@@ -284,12 +303,7 @@ async def get_payments_history(
if not filters:
filters = Filters()
if DB_TYPE == SQLITE and group in sqlite_formats:
date_trunc = f"strftime('{sqlite_formats[group]}', time, 'unixepoch')"
elif group in ("day", "hour", "month"):
date_trunc = f"date_trunc('{group}', time)"
else:
raise ValueError(f"Invalid group value: {group}")
date_trunc = db.datetime_grouping(group)
values = {
"wallet_id": wallet_id,
@@ -341,6 +355,114 @@ async def get_payments_history(
return results
async def get_payment_count_stats(
field: PaymentCountField,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
) -> list[PaymentCountStat]:
if not filters:
filters = Filters()
clause = filters.where()
data = await (conn or db).fetchall(
query=f"""
SELECT {field} as field, count(*) as total
FROM apipayments
{clause}
GROUP BY {field}
ORDER BY {field}
""",
values=filters.values(),
model=PaymentCountStat,
)
return data
async def get_daily_stats(
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
) -> Tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters:
filters = Filters()
in_clause = filters.where(
["(apipayments.status = 'success' AND apipayments.amount > 0)"]
)
out_clause = filters.where(
["(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"]
)
date_trunc = db.datetime_grouping("day")
query = """
SELECT {date_trunc} date,
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance,
ABS(SUM(apipayments.fee)) as fee,
COUNT(*) as payments_count
FROM wallets
LEFT JOIN apipayments ON apipayments.wallet_id = wallets.id
{clause}
AND (wallets.deleted = false OR wallets.deleted is NULL)
GROUP BY date
ORDER BY date ASC
"""
data_in = await (conn or db).fetchall(
query=query.format(date_trunc=date_trunc, clause=in_clause),
values=filters.values(),
model=PaymentDailyStats,
)
data_out = await (conn or db).fetchall(
query=query.format(date_trunc=date_trunc, clause=out_clause),
values=filters.values(),
model=PaymentDailyStats,
)
return data_in, data_out
async def get_wallets_stats(
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
) -> list[PaymentWalletStats]:
if not filters:
filters = Filters()
where_stmts = [
"(wallets.deleted = false OR wallets.deleted is NULL)",
"""
(
(apipayments.status = 'success' AND apipayments.amount > 0)
OR (
apipayments.status IN ('success', 'pending')
AND apipayments.amount < 0
)
)
""",
]
clauses = filters.where(where_stmts)
data = await (conn or db).fetchall(
query=f"""
SELECT apipayments.wallet_id,
MAX(wallets.name) AS wallet_name,
MAX(wallets.user) AS user_id,
COUNT(*) as payments_count,
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
FROM wallets
LEFT JOIN apipayments ON apipayments.wallet_id = wallets.id
{clauses}
GROUP BY apipayments.wallet_id
ORDER BY payments_count
""",
values=filters.values(),
model=PaymentWalletStats,
)
return data
async def delete_wallet_payment(
checking_id: str, wallet_id: str, conn: Optional[Connection] = None
) -> None:
+24
View File
@@ -6,6 +6,7 @@ from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import get_wallets
from lnbits.core.db import db
from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page
from ..models import (
@@ -93,6 +94,8 @@ async def get_accounts(
async def get_account(
user_id: str, conn: Optional[Connection] = None
) -> Optional[Account]:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE id = :id",
{"id": user_id},
@@ -122,6 +125,8 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, conn: Optional[Connection] = None
) -> Optional[Account]:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE username = :username",
{"username": username},
@@ -142,6 +147,8 @@ async def get_account_by_pubkey(
async def get_account_by_email(
email: str, conn: Optional[Connection] = None
) -> Optional[Account]:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE email = :email",
{"email": email},
@@ -185,3 +192,20 @@ async def get_user_from_account(
super_user=account.is_super_user,
has_password=account.password_hash is not None,
)
async def update_user_access_control_list(user_acls: UserAcls):
user_acls.updated_at = datetime.now(timezone.utc)
await db.update("accounts", user_acls)
async def get_user_access_control_lists(
user_id: str, conn: Optional[Connection] = None
) -> UserAcls:
user_acls = await (conn or db).fetchone(
"SELECT id, access_control_list FROM accounts WHERE id = :id",
{"id": user_id},
UserAcls,
)
return user_acls or UserAcls(id=user_id)
+9 -2
View File
@@ -23,6 +23,7 @@ async def create_wallet(
user=user_id,
adminkey=uuid4().hex,
inkey=uuid4().hex,
currency=settings.lnbits_default_accounting_currency or "USD",
)
await (conn or db).insert("wallets", wallet)
return wallet
@@ -134,6 +135,12 @@ async def get_wallets(
)
async def get_wallets_count():
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
row = result.mappings().first()
return row.get("count", 0)
async def get_wallet_for_key(
key: str,
conn: Optional[Connection] = None,
@@ -152,6 +159,6 @@ async def get_wallet_for_key(
async def get_total_balance(conn: Optional[Connection] = None):
result = await (conn or db).execute("SELECT SUM(balance) FROM balances")
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
row = result.mappings().first()
return row.get("balance", 0)
return row.get("balance", 0) or 0
+48 -21
View File
@@ -602,34 +602,46 @@ async def m026_update_payment_table(db: Connection):
async def m027_update_apipayments_data(db: Connection):
result = None
try:
result = await db.execute("SELECT * FROM apipayments")
result = await db.execute("SELECT * FROM apipayments LIMIT 100")
except Exception as exc:
logger.warning("Could not select, trying again after cache cleared.")
logger.debug(exc)
await db.execute("COMMIT")
result = await db.execute("SELECT * FROM apipayments")
offset = 0
limit = 1000
payments: list[dict[Any, Any]] = []
logger.info("Updating payments")
while len(payments) > 0 or offset == 0:
logger.info(f"Updating {offset} to {offset+limit}")
payments = result.mappings().all()
for payment in payments:
tag = None
created_at = payment.get("time")
if payment.get("extra"):
extra = json.loads(payment.get("extra"))
tag = extra.get("tag")
tsph = db.timestamp_placeholder("created_at")
await db.execute(
f"""
UPDATE apipayments
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
WHERE checking_id = :checking_id
""",
{
"tag": tag,
"created_at": created_at,
"checking_id": payment.get("checking_id"),
},
result = await db.execute(
f"SELECT * FROM apipayments ORDER BY time LIMIT {limit} OFFSET {offset}"
)
payments = result.mappings().all()
logger.info(f"Payments count: {len(payments)}")
for payment in payments:
tag = None
created_at = payment.get("time")
if payment.get("extra"):
extra = json.loads(str(payment.get("extra")))
tag = extra.get("tag")
tsph = db.timestamp_placeholder("created_at")
await db.execute(
f"""
UPDATE apipayments
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
WHERE checking_id = :checking_id
""",
{
"tag": tag,
"created_at": created_at,
"checking_id": payment.get("checking_id"),
},
)
offset += limit
logger.info("Payments updated")
async def m028_update_settings(db: Connection):
@@ -684,3 +696,18 @@ async def m029_create_audit_table(db: Connection):
);
"""
)
async def m030_add_user_api_tokens_column(db: Connection):
await db.execute(
"""
ALTER TABLE accounts ADD COLUMN access_control_list TEXT
"""
)
async def m031_add_color_and_icon_to_wallets(db: Connection):
"""
Adds icon and color columns to wallets.
"""
await db.execute("ALTER TABLE wallets ADD COLUMN extra TEXT")
+12
View File
@@ -14,10 +14,15 @@ from .payments import (
DecodePayment,
PayInvoice,
Payment,
PaymentCountField,
PaymentCountStat,
PaymentDailyStats,
PaymentExtra,
PaymentFilters,
PaymentHistoryPoint,
PaymentsStatusCount,
PaymentState,
PaymentWalletStats,
)
from .tinyurl import TinyURL
from .users import (
@@ -36,6 +41,7 @@ from .users import (
UpdateUserPassword,
UpdateUserPubkey,
User,
UserAcls,
UserExtra,
)
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
@@ -62,6 +68,11 @@ __all__ = [
"DecodePayment",
"PayInvoice",
"Payment",
"PaymentCountField",
"PaymentCountStat",
"PaymentDailyStats",
"PaymentsStatusCount",
"PaymentWalletStats",
"PaymentExtra",
"PaymentFilters",
"PaymentHistoryPoint",
@@ -73,6 +84,7 @@ __all__ = [
"Account",
"AccountFilters",
"AccountOverview",
"UserAcls",
"CreateUser",
"RegisterUser",
"LoginUsernamePassword",
+15 -16
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Optional
from pydantic import BaseModel, Field
@@ -10,16 +9,16 @@ from lnbits.settings import settings
class AuditEntry(BaseModel):
component: Optional[str] = None
ip_address: Optional[str] = None
user_id: Optional[str] = None
path: Optional[str] = None
request_type: Optional[str] = None
request_method: Optional[str] = None
request_details: Optional[str] = None
response_code: Optional[str] = None
component: str | None = None
ip_address: str | None = None
user_id: str | None = None
path: str | None = None
request_type: str | None = None
request_method: str | None = None
request_details: str | None = None
response_code: str | None = None
duration: float
delete_at: Optional[datetime] = None
delete_at: datetime | None = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
def __init__(self, **data):
@@ -42,12 +41,12 @@ class AuditFilters(FilterModel):
"duration",
]
ip_address: Optional[str] = None
user_id: Optional[str] = None
path: Optional[str] = None
request_method: Optional[str] = None
response_code: Optional[str] = None
component: Optional[str] = None
ip_address: str | None = None
user_id: str | None = None
path: str | None = None
request_method: str | None = None
response_code: str | None = None
component: str | None = None
class AuditCountStat(BaseModel):
+66 -78
View File
@@ -7,7 +7,7 @@ import os
import shutil
import zipfile
from pathlib import Path
from typing import Any, Optional
from typing import Any
import httpx
from loguru import logger
@@ -29,17 +29,17 @@ class ExplicitRelease(BaseModel):
archive: str
hash: str
dependencies: list[str] = []
repo: Optional[str]
icon: Optional[str]
short_description: Optional[str]
min_lnbits_version: Optional[str]
max_lnbits_version: Optional[str]
html_url: Optional[str] # todo: release_url
warning: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
details_link: Optional[str]
pay_link: Optional[str]
repo: str | None
icon: str | None
short_description: str | None
min_lnbits_version: str | None
max_lnbits_version: str | None
html_url: str | None # todo: release_url
warning: str | None
info_notification: str | None
critical_notification: str | None
details_link: str | None
pay_link: str | None
def is_version_compatible(self):
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -77,9 +77,9 @@ class ExtensionConfig(BaseModel):
name: str
short_description: str
tile: str = ""
warning: Optional[str] = ""
min_lnbits_version: Optional[str]
max_lnbits_version: Optional[str]
warning: str | None = ""
min_lnbits_version: str | None
max_lnbits_version: str | None
def is_version_compatible(self) -> bool:
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -87,7 +87,7 @@ class ExtensionConfig(BaseModel):
@classmethod
async def fetch_github_release_config(
cls, org: str, repo: str, tag_name: str
) -> Optional[ExtensionConfig]:
) -> ExtensionConfig | None:
config_url = (
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
)
@@ -97,28 +97,28 @@ class ExtensionConfig(BaseModel):
class ReleasePaymentInfo(BaseModel):
amount: Optional[int] = None
pay_link: Optional[str] = None
payment_hash: Optional[str] = None
payment_request: Optional[str] = None
amount: int | None = None
pay_link: str | None = None
payment_hash: str | None = None
payment_request: str | None = None
class PayToEnableInfo(BaseModel):
amount: int
amount: int = 0
required: bool = False
wallet: Optional[str] = None
wallet: str | None = None
class UserExtensionInfo(BaseModel):
paid_to_enable: Optional[bool] = False
payment_hash_to_enable: Optional[str] = None
paid_to_enable: bool | None = False
payment_hash_to_enable: str | None = None
class UserExtension(BaseModel):
user: str
extension: str
active: bool
extra: Optional[UserExtensionInfo] = None
extra: UserExtensionInfo | None = None
@property
def is_paid(self) -> bool:
@@ -140,16 +140,14 @@ class UserExtension(BaseModel):
class Extension(BaseModel):
code: str
is_valid: bool
name: Optional[str] = None
short_description: Optional[str] = None
tile: Optional[str] = None
upgrade_hash: Optional[str] = ""
name: str | None = None
short_description: str | None = None
tile: str | None = None
upgrade_hash: str | None = ""
@property
def module_name(self) -> str:
if self.is_upgrade_extension:
if settings.has_default_extension_path:
return f"lnbits.upgrades.{self.code}-{self.upgrade_hash}"
return f"{self.code}-{self.upgrade_hash}"
if settings.has_default_extension_path:
@@ -178,21 +176,21 @@ class ExtensionRelease(BaseModel):
archive: str
source_repo: str
is_github_release: bool = False
hash: Optional[str] = None
min_lnbits_version: Optional[str] = None
max_lnbits_version: Optional[str] = None
is_version_compatible: Optional[bool] = True
html_url: Optional[str] = None
description: Optional[str] = None
warning: Optional[str] = None
repo: Optional[str] = None
icon: Optional[str] = None
details_link: Optional[str] = None
hash: str | None = None
min_lnbits_version: str | None = None
max_lnbits_version: str | None = None
is_version_compatible: bool | None = True
html_url: str | None = None
description: str | None = None
warning: str | None = None
repo: str | None = None
icon: str | None = None
details_link: str | None = None
pay_link: Optional[str] = None
cost_sats: Optional[int] = None
paid_sats: Optional[int] = 0
payment_hash: Optional[str] = None
pay_link: str | None = None
cost_sats: int | None = None
paid_sats: int | None = 0
payment_hash: str | None = None
@property
def archive_url(self) -> str:
@@ -210,8 +208,8 @@ class ExtensionRelease(BaseModel):
self.cost_sats = payment_info.amount if payment_info else None
async def fetch_release_payment_info(
self, amount: Optional[int] = None
) -> Optional[ReleasePaymentInfo]:
self, amount: int | None = None
) -> ReleasePaymentInfo | None:
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
assert url, "Missing URL for payment info."
try:
@@ -283,7 +281,7 @@ class ExtensionRelease(BaseModel):
return [GitHubRepoRelease.parse_obj(r) for r in releases]
@classmethod
async def fetch_release_details(cls, details_link: str) -> Optional[dict]:
async def fetch_release_details(cls, details_link: str) -> dict | None:
try:
async with httpx.AsyncClient() as client:
@@ -302,12 +300,12 @@ class ExtensionRelease(BaseModel):
class ExtensionMeta(BaseModel):
installed_release: Optional[ExtensionRelease] = None
latest_release: Optional[ExtensionRelease] = None
pay_to_enable: Optional[PayToEnableInfo] = None
installed_release: ExtensionRelease | None = None
latest_release: ExtensionRelease | None = None
pay_to_enable: PayToEnableInfo | None = None
payments: list[ReleasePaymentInfo] = []
dependencies: list[str] = []
archive: Optional[str] = None
archive: str | None = None
featured: bool = False
@@ -315,11 +313,11 @@ class InstallableExtension(BaseModel):
id: str
name: str
version: str
active: Optional[bool] = False
short_description: Optional[str] = None
icon: Optional[str] = None
active: bool | None = False
short_description: str | None = None
icon: str | None = None
stars: int = 0
meta: Optional[ExtensionMeta] = None
meta: ExtensionMeta | None = None
@property
def hash(self) -> str:
@@ -343,15 +341,11 @@ class InstallableExtension(BaseModel):
@property
def ext_upgrade_dir(self) -> Path:
return Path(
settings.lnbits_extensions_path, "upgrades", f"{self.id}-{self.hash}"
)
return Path(settings.lnbits_extensions_upgrade_path, f"{self.id}-{self.hash}")
@property
def module_name(self) -> str:
if self.ext_upgrade_dir.is_dir():
if settings.has_default_extension_path:
return f"lnbits.upgrades.{self.id}-{self.hash}"
return f"{self.id}-{self.hash}"
if settings.has_default_extension_path:
@@ -411,9 +405,7 @@ class InstallableExtension(BaseModel):
def extract_archive(self):
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
Path(settings.lnbits_extensions_path, "upgrades").mkdir(
parents=True, exist_ok=True
)
Path(settings.lnbits_extensions_upgrade_path).mkdir(parents=True, exist_ok=True)
tmp_dir = Path(settings.lnbits_data_folder, "unzip-temp", self.hash)
shutil.rmtree(tmp_dir, True)
@@ -460,7 +452,7 @@ class InstallableExtension(BaseModel):
shutil.rmtree(self.ext_upgrade_dir, True)
def check_latest_version(self, release: Optional[ExtensionRelease]):
def check_latest_version(self, release: ExtensionRelease | None):
if not release:
return
if not self.meta or not self.meta.latest_release:
@@ -473,9 +465,7 @@ class InstallableExtension(BaseModel):
):
self.meta.latest_release = release
def find_existing_payment(
self, pay_link: Optional[str]
) -> Optional[ReleasePaymentInfo]:
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
if not pay_link or not self.meta or not self.meta.payments:
return None
return next(
@@ -515,7 +505,7 @@ class InstallableExtension(BaseModel):
@classmethod
async def from_github_release(
cls, github_release: GitHubRelease
) -> Optional[InstallableExtension]:
) -> InstallableExtension | None:
try:
repo, latest_release, config = await cls.fetch_github_repo_info(
github_release.organisation, github_release.repository
@@ -554,7 +544,7 @@ class InstallableExtension(BaseModel):
)
@classmethod
def from_ext_dir(cls, ext_id: str) -> Optional[InstallableExtension]:
def from_ext_dir(cls, ext_id: str) -> InstallableExtension | None:
try:
conf_path = Path(
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
@@ -594,7 +584,6 @@ class InstallableExtension(BaseModel):
cls,
) -> list[InstallableExtension]:
extension_list: list[InstallableExtension] = []
extension_id_list: list[str] = []
for url in settings.lnbits_extensions_manifests:
try:
@@ -615,7 +604,6 @@ class InstallableExtension(BaseModel):
meta.featured = ext.id in manifest.featured
ext.meta = meta
extension_list += [ext]
extension_id_list += [ext.id]
for e in manifest.extensions:
release = ExtensionRelease.from_explicit_release(url, e)
@@ -631,10 +619,10 @@ class InstallableExtension(BaseModel):
meta.featured = ext.id in manifest.featured
ext.meta = meta
extension_list += [ext]
extension_id_list += [e.id]
except Exception as e:
logger.warning(f"Manifest {url} failed with '{e!s}'")
settings.lnbits_all_extensions_ids = {e.id for e in extension_list}
return extension_list
@classmethod
@@ -667,7 +655,7 @@ class InstallableExtension(BaseModel):
@classmethod
async def get_extension_release(
cls, ext_id: str, source_repo: str, archive: str, version: str
) -> Optional[ExtensionRelease]:
) -> ExtensionRelease | None:
all_releases: list[ExtensionRelease] = (
await InstallableExtension.get_extension_releases(ext_id)
)
@@ -718,8 +706,8 @@ class CreateExtension(BaseModel):
archive: str
source_repo: str
version: str
cost_sats: Optional[int] = 0
payment_hash: Optional[str] = None
cost_sats: int | None = 0
payment_hash: str | None = None
class ExtensionDetailsRequest(BaseModel):
@@ -728,7 +716,7 @@ class ExtensionDetailsRequest(BaseModel):
version: str
async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
async def github_api_get(url: str, error_msg: str | None) -> Any:
headers = {"User-Agent": settings.user_agent}
if settings.lnbits_ext_github_token:
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
@@ -740,7 +728,7 @@ async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
return resp.json()
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
def icon_to_github_url(source_repo: str, path: str | None) -> str:
if not path:
return ""
_, _, *rest = path.split("/")
+14 -4
View File
@@ -25,12 +25,12 @@ class Callback(BaseModel):
class BalanceDelta(BaseModel):
lnbits_balance_msats: int
node_balance_msats: int
lnbits_balance_sats: int
node_balance_sats: int
@property
def delta_msats(self):
return self.node_balance_msats - self.lnbits_balance_msats
def delta_sats(self) -> int:
return int(self.lnbits_balance_sats - self.node_balance_sats)
class SimpleStatus(BaseModel):
@@ -38,6 +38,16 @@ class SimpleStatus(BaseModel):
message: str
class SimpleItem(BaseModel):
id: str
name: str
class DbVersion(BaseModel):
db: str
version: int
class Image(BaseModel):
filename: str
directory: str = "library"
+65
View File
@@ -0,0 +1,65 @@
from enum import Enum
from pydantic import BaseModel
class NotificationType(Enum):
server_status = "server_status"
settings_update = "settings_update"
balance_update = "balance_update"
watchdog_check = "watchdog_check"
balance_delta = "balance_delta"
server_start_stop = "server_start_stop"
incoming_payment = "incoming_payment"
outgoing_payment = "outgoing_payment"
text_message = "text_message"
class NotificationMessage(BaseModel):
message_type: NotificationType
values: dict
NOTIFICATION_TEMPLATES = {
"text_message": "{message}",
"server_status": """*SERVER STATUS*
*Up time*: `{up_time}`.
*Accounts*: `{accounts_count}`.
*Wallets*: `{wallets_count}`.
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
*Pending payments*: `{pending_payments_count}`.
*Failed payments*: `{failed_payments_count}`.
*LNbits balance*: `{lnbits_balance_sats}` sats.
*Node balance*: `{node_balance_sats}` sats.""",
"server_start_stop": """*SERVER*
{message}
*Time*: `{up_time}` seconds.
""",
"settings_update": """*SETTINGS UPDATED*
User: `{username}`.
""",
"balance_update": """*WALLET CREDIT/DEBIT*
Wallet `{wallet_name}` balance updated with `{amount}` sats.
*Current balance*: `{balance}` sats.
*Wallet ID*: `{wallet_id}`
""",
"watchdog_check": """*WATCHDOG BALANCE CHECK*
*Delta*: `{delta_sats}` sats.
*LNbits balance*: `{lnbits_balance_sats}` sats.
*Node balance*: `{node_balance_sats}` sats.
*Switching to Void Wallet*: `{switch_to_void_wallet}`.
""",
"balance_delta": """*BALANCE DELTA CHANGED*
*New delta*: `{delta_sats}` sats.
*Old delta*: `{old_delta_sats}` sats.
*LNbits balance*: `{lnbits_balance_sats}` sats.
*Node balance*: `{node_balance_sats}` sats.
""",
"outgoing_payment": """*PAYMENT SENT*
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
*Wallet*: `{wallet_name}` ({wallet_id}).""",
"incoming_payment": """*PAYMENT RECEIVED*
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
*Wallet*: `{wallet_name}` ({wallet_id}).
""",
}
+84 -39
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
from typing import Literal
from fastapi import Query
from pydantic import BaseModel, Field, validator
@@ -28,16 +28,16 @@ class PaymentState(str, Enum):
class PaymentExtra(BaseModel):
comment: Optional[str] = None
success_action: Optional[str] = None
lnurl_response: Optional[str] = None
comment: str | None = None
success_action: str | None = None
lnurl_response: str | None = None
class PayInvoice(BaseModel):
payment_request: str
description: Optional[str] = None
max_sat: Optional[int] = None
extra: Optional[dict] = {}
description: str | None = None
max_sat: int | None = None
extra: dict | None = {}
class CreatePayment(BaseModel):
@@ -46,10 +46,10 @@ class CreatePayment(BaseModel):
bolt11: str
amount_msat: int
memo: str
extra: Optional[dict] = {}
preimage: Optional[str] = None
expiry: Optional[datetime] = None
webhook: Optional[str] = None
extra: dict | None = {}
preimage: str | None = None
expiry: datetime | None = None
webhook: str | None = None
fee: int = 0
@@ -61,13 +61,13 @@ class Payment(BaseModel):
fee: int
bolt11: str
status: str = PaymentState.PENDING
memo: Optional[str] = None
expiry: Optional[datetime] = None
webhook: Optional[str] = None
webhook_status: Optional[int] = None
preimage: Optional[str] = None
tag: Optional[str] = None
extension: Optional[str] = None
memo: str | None = None
expiry: datetime | None = None
webhook: str | None = None
webhook_status: int | None = None
preimage: str | None = None
tag: str | None = None
extension: str | None = None
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -125,22 +125,60 @@ class Payment(BaseModel):
class PaymentFilters(FilterModel):
__search_fields__ = ["memo", "amount"]
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
status: str
checking_id: str
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
status: str | None
tag: str | None
checking_id: str | None
amount: int
fee: int
memo: Optional[str]
memo: str | None
time: datetime
bolt11: str
preimage: str
payment_hash: str
expiry: Optional[datetime]
extra: dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
preimage: str | None
payment_hash: str | None
wallet_id: str | None
class PaymentDataPoint(BaseModel):
date: datetime
count: int
max_amount: int
min_amount: int
average_amount: int
total_amount: int
max_fee: int
min_fee: int
average_fee: int
total_fee: int
PaymentCountField = Literal["status", "tag", "extension", "wallet_id"]
class PaymentCountStat(BaseModel):
field: str = ""
total: float = 0
class PaymentWalletStats(BaseModel):
wallet_id: str = ""
wallet_name: str = ""
user_id: str = ""
payments_count: int
balance: float = 0
class PaymentDailyStats(BaseModel):
date: datetime
balance: float = 0
balance_in: float | None = 0
balance_out: float | None = 0
payments_count: int = 0
count_in: int | None = 0
count_out: int | None = 0
fee: float = 0
class PaymentHistoryPoint(BaseModel):
@@ -152,7 +190,7 @@ class PaymentHistoryPoint(BaseModel):
class DecodePayment(BaseModel):
data: str
filter_fields: Optional[list[str]] = []
filter_fields: list[str] | None = []
class CreateInvoice(BaseModel):
@@ -160,14 +198,14 @@ class CreateInvoice(BaseModel):
internal: bool = False
out: bool = True
amount: float = Query(None, ge=0)
memo: Optional[str] = None
description_hash: Optional[str] = None
unhashed_description: Optional[str] = None
expiry: Optional[int] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
bolt11: Optional[str] = None
lnurl_callback: Optional[str] = None
memo: str | None = Query(None, max_length=640)
description_hash: str | None = None
unhashed_description: str | None = None
expiry: int | None = None
extra: dict | None = None
webhook: str | None = None
bolt11: str | None = None
lnurl_callback: str | None = None
@validator("unit")
@classmethod
@@ -175,3 +213,10 @@ class CreateInvoice(BaseModel):
if v != "sat" and v not in allowed_currencies():
raise ValueError("The provided unit is not supported")
return v
class PaymentsStatusCount(BaseModel):
incoming: int = 0
outgoing: int = 0
failed: int = 0
pending: int = 0
+128 -39
View File
@@ -1,13 +1,13 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Optional
from uuid import UUID
from fastapi import Query
from passlib.context import CryptContext
from pydantic import BaseModel, Field
from lnbits.core.models.misc import SimpleItem
from lnbits.db import FilterModel
from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username
from lnbits.settings import settings
@@ -16,25 +16,86 @@ from .wallets import Wallet
class UserExtra(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
email_verified: bool | None = False
first_name: str | None = None
last_name: str | None = None
display_name: str | None = None
picture: str | None = 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
provider: str | None = "lnbits" # auth provider
class EndpointAccess(BaseModel):
path: str
name: str
read: bool = False
write: bool = False
def supports_method(self, method: str) -> bool:
# all http methods
if method in ["GET", "OPTIONS", "HEAD"]:
return self.read
if method in ["POST", "PUT", "PATCH", "DELETE"]:
return self.write
return False
class AccessControlList(BaseModel):
id: str
name: str
endpoints: list[EndpointAccess] = []
token_id_list: list[SimpleItem] = []
def get_endpoint(self, path: str) -> EndpointAccess | None:
for e in self.endpoints:
if e.path == path:
return e
return None
def get_token_by_id(self, token_id: str) -> SimpleItem | None:
for t in self.token_id_list:
if t.id == token_id:
return t
return None
def delete_token_by_id(self, token_id: str):
self.token_id_list = [t for t in self.token_id_list if t.id != token_id]
class UserAcls(BaseModel):
id: str
access_control_list: list[AccessControlList] = []
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
def get_acl_by_id(self, acl_id: str) -> AccessControlList | None:
for acl in self.access_control_list:
if acl.id == acl_id:
return acl
return None
def delete_acl_by_id(self, acl_id: str):
self.access_control_list = [
acl for acl in self.access_control_list if acl.id != acl_id
]
def get_acl_by_token_id(self, token_id: str) -> AccessControlList | None:
for acl in self.access_control_list:
if acl.get_token_by_id(token_id):
return acl
return None
class Account(BaseModel):
id: str
username: Optional[str] = None
password_hash: Optional[str] = None
pubkey: Optional[str] = None
email: Optional[str] = None
username: str | None = None
password_hash: str | None = None
pubkey: str | None = None
email: str | None = None
extra: UserExtra = UserExtra()
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -72,10 +133,10 @@ class Account(BaseModel):
class AccountOverview(Account):
transaction_count: Optional[int] = 0
wallet_count: Optional[int] = 0
balance_msat: Optional[int] = 0
last_payment: Optional[datetime] = None
transaction_count: int | None = 0
wallet_count: int | None = 0
balance_msat: int | None = 0
last_payment: datetime | None = None
class AccountFilters(FilterModel):
@@ -89,20 +150,20 @@ class AccountFilters(FilterModel):
"last_payment",
]
email: Optional[str] = None
user: Optional[str] = None
username: Optional[str] = None
pubkey: Optional[str] = None
wallet_id: Optional[str] = None
email: str | None = None
user: str | None = None
username: str | None = None
pubkey: str | None = None
wallet_id: str | None = None
class User(BaseModel):
id: str
created_at: datetime
updated_at: datetime
email: Optional[str] = None
username: Optional[str] = None
pubkey: Optional[str] = None
email: str | None = None
username: str | None = None
pubkey: str | None = None
extensions: list[str] = []
wallets: list[Wallet] = []
admin: bool = False
@@ -114,7 +175,7 @@ class User(BaseModel):
def wallet_ids(self) -> list[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional[Wallet]:
def get_wallet(self, wallet_id: str) -> Wallet | None:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
@@ -130,33 +191,33 @@ class User(BaseModel):
class RegisterUser(BaseModel):
email: Optional[str] = Query(default=None)
email: str | None = 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 CreateUser(BaseModel):
id: Optional[str] = Query(default=None)
email: Optional[str] = Query(default=None)
username: Optional[str] = Query(default=None, min_length=2, max_length=20)
password: Optional[str] = Query(default=None, min_length=8, max_length=50)
password_repeat: Optional[str] = Query(default=None, min_length=8, max_length=50)
id: str | None = Query(default=None)
email: str | None = Query(default=None)
username: str | None = Query(default=None, min_length=2, max_length=20)
password: str | None = Query(default=None, min_length=8, max_length=50)
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
pubkey: str = Query(default=None, max_length=64)
extensions: Optional[list[str]] = None
extra: Optional[UserExtra] = None
extensions: list[str] | None = None
extra: UserExtra | None = None
class UpdateUser(BaseModel):
user_id: str
email: Optional[str] = Query(default=None)
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
extra: Optional[UserExtra] = None
email: str | None = Query(default=None)
username: str | None = Query(default=..., min_length=2, max_length=20)
extra: UserExtra | None = None
class UpdateUserPassword(BaseModel):
user_id: str
password_old: Optional[str] = None
password_old: str | None = None
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
username: str = Query(default=..., min_length=2, max_length=20)
@@ -190,11 +251,39 @@ class LoginUsernamePassword(BaseModel):
class AccessTokenPayload(BaseModel):
sub: str
usr: Optional[str] = None
email: Optional[str] = None
auth_time: Optional[int] = 0
usr: str | None = None
email: str | None = None
auth_time: int | None = 0
api_token_id: str | None = None
class UpdateBalance(BaseModel):
id: str
amount: int
class ApiTokenRequest(BaseModel):
acl_id: str
token_name: str
password: str
expiration_time_minutes: int
class ApiTokenResponse(BaseModel):
id: str
api_token: str
class UpdateAccessControlList(AccessControlList):
password: str
class DeleteAccessControlList(BaseModel):
id: str
password: str
class DeleteTokenRequest(BaseModel):
id: str
acl_id: str
password: str
+8 -3
View File
@@ -5,7 +5,6 @@ import hmac
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
from ecdsa import SECP256k1, SigningKey
from pydantic import BaseModel, Field
@@ -23,6 +22,11 @@ class BaseWallet(BaseModel):
balance_msat: int
class WalletExtra(BaseModel):
icon: str = "flash_on"
color: str = "primary"
class Wallet(BaseModel):
id: str
user: str
@@ -32,8 +36,9 @@ class Wallet(BaseModel):
deleted: bool = False
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
currency: Optional[str] = None
currency: str | None = None
balance_msat: int = Field(default=0, no_database=True)
extra: WalletExtra = WalletExtra()
@property
def balance(self) -> int:
@@ -61,7 +66,7 @@ class Wallet(BaseModel):
class CreateWallet(BaseModel):
name: Optional[str] = None
name: str | None = None
class KeyType(Enum):
+4 -4
View File
@@ -3,6 +3,7 @@ from .funding_source import (
switch_to_voidwallet,
)
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
from .notifications import enqueue_notification, send_payment_notification
from .payments import (
calculate_fiat_amounts,
check_transaction_status,
@@ -11,7 +12,6 @@ from .payments import (
fee_reserve,
fee_reserve_total,
pay_invoice,
send_payment_notification,
service_fee,
update_pending_payments,
update_wallet_balance,
@@ -24,7 +24,6 @@ from .users import (
check_admin_settings,
create_user_account,
create_user_account_no_ckeck,
init_admin_settings,
update_user_account,
update_user_extensions,
)
@@ -37,6 +36,9 @@ __all__ = [
# lnurl
"redeem_lnurl_withdraw",
"perform_lnurlauth",
# notifications
"enqueue_notification",
"send_payment_notification",
# payments
"calculate_fiat_amounts",
"check_transaction_status",
@@ -45,7 +47,6 @@ __all__ = [
"fee_reserve",
"fee_reserve_total",
"pay_invoice",
"send_payment_notification",
"service_fee",
"update_pending_payments",
"update_wallet_balance",
@@ -56,7 +57,6 @@ __all__ = [
"check_admin_settings",
"create_user_account",
"create_user_account_no_ckeck",
"init_admin_settings",
"update_user_account",
"update_user_extensions",
# websockets
+62 -2
View File
@@ -1,3 +1,7 @@
from loguru import logger
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.notifications import enqueue_notification
from lnbits.settings import settings
from lnbits.wallets import get_funding_source, set_funding_source
@@ -18,6 +22,62 @@ async def get_balance_delta() -> BalanceDelta:
status = await funding_source.status()
lnbits_balance = await get_total_balance()
return BalanceDelta(
lnbits_balance_msats=lnbits_balance,
node_balance_msats=status.balance_msat,
lnbits_balance_sats=int(lnbits_balance) // 1000,
node_balance_sats=status.balance_msat // 1000,
)
async def check_server_balance_against_node():
"""
Watchdog will check lnbits balance and nodebalance
and will switch to VoidWallet if the watchdog delta is reached.
"""
if (
not settings.lnbits_watchdog_switch_to_voidwallet
and not settings.lnbits_notification_watchdog
):
return
funding_source = get_funding_source()
if funding_source.__class__.__name__ == "VoidWallet":
return
status = await get_balance_delta()
if status.delta_sats < settings.lnbits_watchdog_delta:
return
use_voidwallet = settings.lnbits_watchdog_switch_to_voidwallet
logger.warning(
f"Balance delta reached: {status.delta_sats} sats."
f" Switch to void wallet: {use_voidwallet}."
)
enqueue_notification(
NotificationType.watchdog_check,
{
"delta_sats": status.delta_sats,
"lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats,
"switch_to_void_wallet": use_voidwallet,
},
)
if use_voidwallet:
logger.error(f"Switching to VoidWallet. Delta: {status.delta_sats} sats.")
await switch_to_voidwallet()
async def check_balance_delta_changed():
status = await get_balance_delta()
if settings.latest_balance_delta_sats is None:
settings.latest_balance_delta_sats = status.delta_sats
return
if status.delta_sats != settings.latest_balance_delta_sats:
enqueue_notification(
NotificationType.balance_delta,
{
"delta_sats": status.delta_sats,
"old_delta_sats": settings.latest_balance_delta_sats,
"lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats,
},
)
settings.latest_balance_delta_sats = status.delta_sats
+4 -1
View File
@@ -13,7 +13,7 @@ from lnbits.decorators import (
WalletTypeInfo,
require_admin_key,
)
from lnbits.helpers import url_for
from lnbits.helpers import check_callback_url, url_for
from lnbits.lnurl import LnurlErrorResponse
from lnbits.lnurl import decode as decode_lnurl
from lnbits.settings import settings
@@ -37,6 +37,7 @@ async def redeem_lnurl_withdraw(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
lnurl = decode_lnurl(lnurl_request)
check_callback_url(str(lnurl))
r = await client.get(str(lnurl))
res = r.json()
@@ -72,6 +73,7 @@ async def redeem_lnurl_withdraw(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(res["callback"])
await client.get(res["callback"], params=params)
except Exception:
pass
@@ -135,6 +137,7 @@ async def perform_lnurlauth(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
assert key.verifying_key, "LNURLauth verifying_key does not exist"
check_callback_url(callback)
r = await client.get(
callback,
params={
+73
View File
@@ -0,0 +1,73 @@
import asyncio
from typing import Tuple
import httpx
from loguru import logger
from pynostr.encrypted_dm import EncryptedDirectMessage
from websocket import WebSocket, create_connection
from lnbits.core.helpers import is_valid_url
from lnbits.utils.nostr import (
validate_identifier,
validate_pub_key,
)
async def send_nostr_dm(
from_private_key_hex: str,
to_pubkey_hex: str,
message: str,
relays: list[str],
) -> dict:
dm = EncryptedDirectMessage()
dm.encrypt(
private_key_hex=from_private_key_hex,
recipient_pubkey=to_pubkey_hex,
cleartext_content=message,
)
dm_event = dm.to_event()
dm_event.sign(private_key_hex=from_private_key_hex)
notification = dm_event.to_message()
ws_connections: list[WebSocket] = []
for relay in relays:
try:
ws = create_connection(relay, timeout=2)
ws.send(notification)
ws_connections.append(ws)
except Exception as e:
logger.warning(f"Error sending notification to relay {relay}: {e}")
await asyncio.sleep(1)
for ws in ws_connections:
try:
ws.close()
except Exception as e:
logger.debug(f"Failed to close websocket connection: {e}")
return dm_event.to_dict()
async def fetch_nip5_details(identifier: str) -> Tuple[str, list[str]]:
identifier, domain = identifier.split("@")
if not identifier or not domain:
raise ValueError("Invalid NIP5 identifier")
if not is_valid_url(f"https://{domain}"):
raise ValueError("Invalid NIP5 domain")
validate_identifier(identifier)
url = f"https://{domain}/.well-known/nostr.json?name={identifier}"
async with httpx.AsyncClient() as client:
resp = await client.get(url)
resp.raise_for_status()
data = resp.json()
if "names" not in data or identifier not in data["names"]:
raise ValueError("NIP5 not name found")
pubkey = data["names"][identifier]
validate_pub_key(pubkey)
relays = data["relays"].get(pubkey, []) if "relays" in data else []
return pubkey, relays
+330
View File
@@ -0,0 +1,330 @@
import asyncio
import json
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from http import HTTPStatus
from typing import Optional, Tuple
import httpx
from loguru import logger
from py_vapid import Vapid
from pywebpush import WebPushException, webpush
from lnbits.core.crud import (
delete_webpush_subscriptions,
get_webpush_subscriptions_for_user,
mark_webhook_sent,
)
from lnbits.core.models import Payment, Wallet
from lnbits.core.models.notifications import (
NOTIFICATION_TEMPLATES,
NotificationMessage,
NotificationType,
)
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
from lnbits.core.services.websockets import websocket_manager
from lnbits.helpers import check_callback_url, is_valid_email_address
from lnbits.settings import settings
from lnbits.utils.nostr import normalize_private_key
notifications_queue: asyncio.Queue = asyncio.Queue()
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
if not is_message_type_enabled(message_type):
return
try:
notifications_queue.put_nowait(
NotificationMessage(message_type=message_type, values=values)
)
except Exception as e:
logger.error(f"Error enqueuing notification: {e}")
async def process_next_notification():
notification_message: NotificationMessage = await notifications_queue.get()
message_type, text = _notification_message_to_text(notification_message)
await send_notification(text, message_type)
async def send_notification(
message: str,
message_type: Optional[str] = None,
) -> None:
try:
if settings.lnbits_telegram_notifications_enabled:
await send_telegram_notification(message)
logger.debug(f"Sent telegram notification: {message_type}")
except Exception as e:
logger.error(f"Error sending telegram notification {message_type}: {e}")
try:
if settings.lnbits_nostr_notifications_enabled:
await send_nostr_notification(message)
logger.debug(f"Sent nostr notification: {message_type}")
except Exception as e:
logger.error(f"Error sending nostr notification {message_type}: {e}")
try:
if settings.lnbits_email_notifications_enabled:
await send_email_notification(message)
logger.debug(f"Sent email notification: {message_type}")
except Exception as e:
logger.error(f"Error sending email notification {message_type}: {e}")
async def send_nostr_notification(message: str) -> dict:
for i in settings.lnbits_nostr_notifications_identifiers:
try:
identifier = await fetch_nip5_details(i)
user_pubkey = identifier[0]
relays = identifier[1]
server_private_key = normalize_private_key(
settings.lnbits_nostr_notifications_private_key
)
await send_nostr_dm(
server_private_key,
user_pubkey,
message,
relays,
)
except Exception as e:
logger.warning(f"Error notifying identifier {i}: {e}")
return {"status": "ok"}
async def send_telegram_notification(message: str) -> dict:
return await send_telegram_message(
settings.lnbits_telegram_notifications_access_token,
settings.lnbits_telegram_notifications_chat_id,
message,
)
async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
url = f"https://api.telegram.org/bot{token}/sendMessage"
payload = {"chat_id": chat_id, "text": message, "parse_mode": "markdown"}
async with httpx.AsyncClient() as client:
response = await client.post(url, data=payload)
response.raise_for_status()
return response.json()
async def send_email_notification(
message: str, subject: str = "LNbits Notification"
) -> dict:
if not settings.lnbits_email_notifications_enabled:
return {"status": "error", "message": "Email notifications are disabled"}
try:
await send_email(
settings.lnbits_email_notifications_server,
settings.lnbits_email_notifications_port,
settings.lnbits_email_notifications_username,
settings.lnbits_email_notifications_password,
settings.lnbits_email_notifications_email,
settings.lnbits_email_notifications_to_emails,
subject,
message,
)
return {"status": "ok"}
except Exception as e:
logger.error(f"Error sending email notification: {e}")
return {"status": "error", "message": str(e)}
async def send_email(
server: str,
port: int,
username: str,
password: str,
from_email: str,
to_emails: list[str],
subject: str,
message: str,
) -> bool:
if not is_valid_email_address(from_email):
raise ValueError(f"Invalid from email address: {from_email}")
if len(to_emails) == 0:
raise ValueError("No email addresses provided")
for email in to_emails:
if not is_valid_email_address(email):
raise ValueError(f"Invalid email address: {email}")
msg = MIMEMultipart()
msg["From"] = from_email
msg["To"] = ", ".join(to_emails)
msg["Subject"] = subject
msg.attach(MIMEText(message, "plain"))
username = username if len(username) > 0 else from_email
with smtplib.SMTP(server, port) as smtp_server:
smtp_server.starttls()
smtp_server.login(username, password)
smtp_server.sendmail(from_email, to_emails, msg.as_string())
return True
def is_message_type_enabled(message_type: NotificationType) -> bool:
if message_type == NotificationType.balance_update:
return settings.lnbits_notification_credit_debit
if message_type == NotificationType.settings_update:
return settings.lnbits_notification_settings_update
if message_type == NotificationType.watchdog_check:
return settings.lnbits_notification_watchdog
if message_type == NotificationType.balance_delta:
return settings.notification_balance_delta_changed
if message_type == NotificationType.server_start_stop:
return settings.lnbits_notification_server_start_stop
if message_type == NotificationType.server_status:
return settings.lnbits_notification_server_status_hours > 0
if message_type == NotificationType.incoming_payment:
return settings.lnbits_notification_incoming_payment_amount_sats > 0
if message_type == NotificationType.outgoing_payment:
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
return False
def _notification_message_to_text(
notification_message: NotificationMessage,
) -> Tuple[str, str]:
message_type = notification_message.message_type.value
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
try:
text = meesage_value.format(**notification_message.values)
except Exception as e:
logger.warning(f"Error formatting notification message: {e}")
text = meesage_value
text = f"""[{settings.lnbits_site_title}]\n{text}"""
return message_type, text
async def dispatch_webhook(payment: Payment):
"""
Dispatches the webhook to the webhook url.
"""
logger.debug("sending webhook", payment.webhook)
if not payment.webhook:
return await mark_webhook_sent(payment.payment_hash, -1)
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
data = payment.dict()
try:
check_callback_url(payment.webhook)
r = await client.post(payment.webhook, json=data, timeout=40)
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_notification(wallet: Wallet, payment: Payment):
try:
await send_ws_payment_notification(wallet, payment)
except Exception as e:
logger.error("Error sending websocket payment notification", e)
try:
send_chat_payment_notification(wallet, payment)
except Exception as e:
logger.error("Error sending chat payment notification", e)
try:
await send_payment_push_notification(wallet, payment)
except Exception as e:
logger.error("Error sending push payment notification", e)
if payment.webhook and not payment.webhook_status:
await dispatch_webhook(payment)
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
# TODO: websocket message should be a clean payment model
# await websocket_manager.send_data(payment.json(), wallet.inkey)
# TODO: figure out why we send the balance with the payment here.
# cleaner would be to have a separate message for the balance
# and send it with the id of the wallet so wallets can subscribe to it
payment_notification = json.dumps(
{
"wallet_balance": wallet.balance,
# use pydantic json serialization to get the correct datetime format
"payment": json.loads(payment.json()),
},
)
await websocket_manager.send_data(payment_notification, wallet.inkey)
await websocket_manager.send_data(payment_notification, wallet.adminkey)
await websocket_manager.send_data(
json.dumps({"pending": payment.pending, "status": payment.status}),
payment.payment_hash,
)
def send_chat_payment_notification(wallet: Wallet, payment: Payment):
amount_sats = abs(payment.sat)
values: dict = {
"wallet_id": wallet.id,
"wallet_name": wallet.name,
"amount_sats": amount_sats,
"fiat_value_fmt": "",
}
if payment.extra.get("wallet_fiat_currency", None):
amount_fiat = payment.extra.get("wallet_fiat_amount", None)
currency = payment.extra.get("wallet_fiat_currency", None)
values["fiat_value_fmt"] = f"`{amount_fiat}`*{currency}* / "
if payment.is_out:
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
enqueue_notification(NotificationType.outgoing_payment, values)
else:
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
enqueue_notification(NotificationType.incoming_payment, values)
async def send_payment_push_notification(wallet: Wallet, payment: Payment):
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
amount = int(payment.amount / 1000)
title = f"LNbits: {wallet.name}"
body = f"You just received {amount} sat{'s'[:amount^1]}!"
if payment.memo:
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}"
await send_push_notification(subscription, title, body, url)
async def send_push_notification(subscription, title, body, url=""):
vapid = Vapid()
try:
logger.debug("sending push notification")
webpush(
json.loads(subscription.data),
json.dumps({"title": title, "body": body, "url": url}),
(
vapid.from_pem(bytes(settings.lnbits_webpush_privkey, "utf-8"))
if settings.lnbits_webpush_privkey
else None
),
{"aud": "", "sub": "mailto:alan@lnbits.com"},
)
except WebPushException as e:
if e.response and e.response.status_code == HTTPStatus.GONE:
# cleanup unsubscribed or expired push subscriptions
await delete_webpush_subscriptions(subscription.endpoint)
else:
logger.error(
f"failed sending push notification: "
f"{e.response.text if e.response else e}"
)
+126 -52
View File
@@ -1,5 +1,6 @@
import json
import asyncio
import time
from datetime import datetime, timedelta, timezone
from typing import Optional
from bolt11 import Bolt11, MilliSatoshi, Tags
@@ -7,11 +8,14 @@ from bolt11 import decode as bolt11_decode
from bolt11 import encode as bolt11_encode
from loguru import logger
from lnbits.core.crud.payments import get_daily_stats
from lnbits.core.db import db
from lnbits.db import Connection
from lnbits.core.models import PaymentDailyStats, PaymentFilters
from lnbits.db import Connection, Filters
from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import settings
from lnbits.tasks import create_task
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import fake_wallet, get_funding_source
@@ -38,7 +42,7 @@ from ..models import (
PaymentState,
Wallet,
)
from .websockets import websocket_manager
from .notifications import send_payment_notification
async def pay_invoice(
@@ -51,14 +55,16 @@ async def pay_invoice(
tag: str = "",
conn: Optional[Connection] = None,
) -> Payment:
if settings.lnbits_only_allow_incoming_payments:
raise PaymentError("Only incoming payments allowed.", status="failed")
invoice = _validate_payment_request(payment_request, max_sat)
assert invoice.amount_msat
async with db.reuse_conn(conn) if conn else db.connect() as conn:
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
amount_msat = invoice.amount_msat
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, conn)
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
if await is_internal_status_success(invoice.payment_hash, conn):
if await is_internal_status_success(invoice.payment_hash, new_conn):
raise PaymentError("Internal invoice already paid.", status="failed")
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
@@ -73,8 +79,10 @@ async def pay_invoice(
extra=extra,
)
payment = await _pay_invoice(wallet, create_payment_model, conn)
await _credit_service_fee_wallet(payment, conn)
payment = await _pay_invoice(wallet, create_payment_model, conn)
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
await _credit_service_fee_wallet(payment, new_conn)
return payment
@@ -100,7 +108,7 @@ async def create_invoice(
if not user_wallet:
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
invoice_memo = None if description_hash else memo
invoice_memo = None if description_hash else memo[:640]
# use the fake wallet if the invoice is for internal use only
funding_source = fake_wallet if internal else get_funding_source()
@@ -109,6 +117,12 @@ async def create_invoice(
amount, user_wallet, currency, extra
)
if amount_sat > settings.lnbits_max_incoming_payment_amount_sats:
raise InvoiceError(
f"Invoice amount {amount_sat} sats is too high. Max allowed: "
f"{settings.lnbits_max_incoming_payment_amount_sats} sats.",
status="failed",
)
if settings.is_wallet_max_balance_exceeded(
user_wallet.balance_msat / 1000 + amount_sat
):
@@ -164,13 +178,20 @@ async def update_pending_payments(wallet_id: str):
exclude_uncheckable=True,
)
for payment in pending_payments:
status = await payment.check_status()
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment)
elif status.success:
payment.status = PaymentState.SUCCESS
await update_payment(payment)
await update_pending_payment(payment)
async def update_pending_payment(payment: Payment) -> bool:
status = await payment.check_status()
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment)
return True
if status.success:
payment.status = PaymentState.SUCCESS
await update_payment(payment)
return True
return False
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
@@ -261,27 +282,6 @@ async def update_wallet_balance(
await internal_invoice_queue.put(payment.checking_id)
async def send_payment_notification(wallet: Wallet, payment: Payment):
# TODO: websocket message should be a clean payment model
# await websocket_manager.send_data(payment.json(), wallet.inkey)
# TODO: figure out why we send the balance with the payment here.
# cleaner would be to have a separate message for the balance
# and send it with the id of the wallet so wallets can subscribe to it
payment_notification = json.dumps(
{
"wallet_balance": wallet.balance,
# use pydantic json serialization to get the correct datetime format
"payment": json.loads(payment.json()),
},
)
await websocket_manager.send_data(payment_notification, wallet.inkey)
await websocket_manager.send_data(payment_notification, wallet.adminkey)
await websocket_manager.send_data(
json.dumps({"pending": payment.pending}), payment.payment_hash
)
async def check_wallet_limits(
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
):
@@ -354,17 +354,22 @@ async def calculate_fiat_amounts(
fiat_amounts["fiat_currency"] = currency
fiat_amounts["fiat_amount"] = round(amount, ndigits=3)
fiat_amounts["fiat_rate"] = amount_sat / amount
fiat_amounts["btc_rate"] = (amount / amount_sat) * 100_000_000
else:
amount_sat = int(amount)
if wallet_currency:
if wallet_currency == currency:
fiat_amount = amount
else:
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
fiat_amounts["wallet_fiat_currency"] = wallet_currency
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount
try:
if wallet_currency == currency:
fiat_amount = amount
else:
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
fiat_amounts["wallet_fiat_currency"] = wallet_currency
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount
fiat_amounts["wallet_btc_rate"] = (fiat_amount / amount_sat) * 100_000_000
except Exception as e:
logger.error(f"Error calculating fiat amount for wallet '{wallet.id}': {e}")
logger.debug(
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}"
@@ -388,6 +393,50 @@ async def check_transaction_status(
return await payment.check_status()
async def get_payments_daily_stats(
filters: Filters[PaymentFilters],
) -> list[PaymentDailyStats]:
data_in, data_out = await get_daily_stats(filters)
balance_total: float = 0
_none = PaymentDailyStats(date=datetime.now(timezone.utc))
if len(data_in) == 0 and len(data_out) == 0:
return []
if len(data_in) == 0:
data_in = [_none]
if len(data_out) == 0:
data_out = [_none]
data: list[PaymentDailyStats] = []
start_date = min(data_in[0].date, data_out[0].date)
end_date = max(data_in[-1].date, data_out[-1].date)
delta = timedelta(days=1)
while start_date <= end_date:
data_in_point = next((x for x in data_in if x.date == start_date), _none)
data_out_point = next((x for x in data_out if x.date == start_date), _none)
balance_total += data_in_point.balance + data_out_point.balance
data.append(
PaymentDailyStats(
date=start_date,
balance=balance_total // 1000,
balance_in=data_in_point.balance // 1000,
balance_out=data_out_point.balance // 1000,
payments_count=data_in_point.payments_count
+ data_out_point.payments_count,
count_in=data_in_point.payments_count,
count_out=data_out_point.payments_count,
fee=(data_in_point.fee + data_out_point.fee) // 1000,
)
)
start_date += delta
return data
async def _pay_invoice(wallet, create_payment_model, conn):
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
if not payment:
@@ -491,13 +540,19 @@ async def _pay_external_invoice(
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
service_fee_msat = service_fee(amount_msat, internal=False)
funding_source = get_funding_source()
logger.debug(f"fundingsource: sending payment {checking_id}")
payment_response: PaymentResponse = await funding_source.pay_invoice(
create_payment_model.bolt11, fee_reserve_msat
task = create_task(
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
)
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
# make sure a hold invoice or deferred payment is not blocking the server
try:
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds)
payment_response = await asyncio.wait_for(task, wait_time)
except asyncio.TimeoutError:
# return pending payment on timeout
logger.debug(f"payment timeout, {checking_id} is still pending")
return payment
if payment_response.checking_id and payment_response.checking_id != checking_id:
logger.warning(
f"backend sent unexpected checking_id (expected: {checking_id} got:"
@@ -533,9 +588,22 @@ async def _pay_external_invoice(
"didn't receive checking_id from backend, payment may be stuck in"
f" database: {checking_id}"
)
return payment
async def _fundingsource_pay_invoice(
checking_id: str, bolt11: str, fee_reserve_msat: int
) -> PaymentResponse:
logger.debug(f"fundingsource: sending payment {checking_id}")
funding_source = get_funding_source()
payment_response: PaymentResponse = await funding_source.pay_invoice(
bolt11, fee_reserve_msat
)
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
return payment_response
async def _verify_external_payment(
payment: Payment, conn: Optional[Connection] = None
) -> Payment:
@@ -595,8 +663,14 @@ def _validate_payment_request(
if not invoice.amount_msat or not invoice.amount_msat > 0:
raise PaymentError("Amountless invoices not supported.", status="failed")
if max_sat and invoice.amount_msat > max_sat * 1000:
raise PaymentError("Amount in invoice is too high.", status="failed")
max_sat = max_sat or settings.lnbits_max_outgoing_payment_amount_sats
max_sat = min(max_sat, settings.lnbits_max_outgoing_payment_amount_sats)
if invoice.amount_msat > max_sat * 1000:
raise PaymentError(
f"Invoice amount {invoice.amount_msat // 1000} sats is too high. "
f"Max allowed: {max_sat} sats.",
status="failed",
)
return invoice
+6 -1
View File
@@ -6,6 +6,7 @@ from py_vapid.utils import b64urlencode
from lnbits.db import dict_to_model
from lnbits.settings import (
EditableSettings,
UpdateSettings,
readonly_variables,
settings,
)
@@ -37,8 +38,12 @@ async def check_webpush_settings():
logger.info(f"Pubkey: {settings.lnbits_webpush_pubkey}")
def dict_to_settings(sets_dict: dict) -> UpdateSettings:
return dict_to_model(sets_dict, UpdateSettings)
def update_cached_settings(sets_dict: dict):
editable_settings = dict_to_model(sets_dict, EditableSettings)
editable_settings = dict_to_settings(sets_dict)
for key in sets_dict.keys():
if key in readonly_variables:
continue
+10 -13
View File
@@ -8,7 +8,6 @@ from lnbits.core.models.extensions import UserExtension
from lnbits.settings import (
EditableSettings,
SuperSettings,
send_admin_user_to_saas,
settings,
)
@@ -47,7 +46,9 @@ async def create_user_account(
async def create_user_account_no_ckeck(
account: Optional[Account] = None, wallet_name: Optional[str] = None
account: Optional[Account] = None,
wallet_name: Optional[str] = None,
default_exts: Optional[list[str]] = None,
) -> User:
if account:
@@ -70,9 +71,13 @@ async def create_user_account_no_ckeck(
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
)
for ext_id in settings.lnbits_user_default_extensions:
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
await update_user_extension(user_ext)
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
for ext_id in user_extensions:
try:
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
await create_user_extension(user_ext)
except Exception as e:
logger.error(f"Error enabeling default extension {ext_id}: {e}")
user = await get_user_from_account(account)
assert user, "Cannot find user for account."
@@ -154,14 +159,6 @@ async def check_admin_settings():
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
file.write(settings.super_user)
# callback for saas
if (
settings.lnbits_saas_callback
and settings.lnbits_saas_secret
and settings.lnbits_saas_instance_id
):
send_admin_user_to_saas()
account = await get_account(settings.super_user)
if account and account.extra and account.extra.provider == "env":
settings.first_install = True
+91 -112
View File
@@ -1,79 +1,92 @@
import asyncio
import traceback
from typing import Callable, Coroutine
import httpx
from loguru import logger
from lnbits.core.crud import (
create_audit_entry,
get_wallet,
get_webpush_subscriptions_for_user,
mark_webhook_sent,
)
from lnbits.core.crud.audit import delete_expired_audit_entries
from lnbits.core.models import AuditEntry, Payment
from lnbits.core.services import (
from lnbits.core.crud.payments import get_payments_status_count
from lnbits.core.crud.users import get_accounts
from lnbits.core.crud.wallets import get_wallets_count
from lnbits.core.models import AuditEntry
from lnbits.core.models.extensions import InstallableExtension
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.funding_source import (
check_balance_delta_changed,
check_server_balance_against_node,
get_balance_delta,
send_payment_notification,
switch_to_voidwallet,
)
from lnbits.settings import get_funding_source, settings
from lnbits.tasks import send_push_notification
from lnbits.core.services.notifications import (
enqueue_notification,
process_next_notification,
send_payment_notification,
)
from lnbits.db import Filters
from lnbits.settings import settings
from lnbits.tasks import create_unique_task
from lnbits.utils.exchange_rates import btc_rates
audit_queue: asyncio.Queue = 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.
"""
async def run_by_the_minute_tasks():
minute_counter = 0
while settings.lnbits_running:
funding_source = get_funding_source()
if (
settings.lnbits_killswitch
and funding_source.__class__.__name__ != "VoidWallet"
):
with httpx.Client() as client:
try:
r = client.get(settings.lnbits_status_manifest, timeout=4)
r.raise_for_status()
if r.status_code == 200:
ks = r.json().get("killswitch")
if ks and ks == 1:
logger.error(
"Switching to VoidWallet. Killswitch triggered."
)
await switch_to_voidwallet()
except (httpx.RequestError, httpx.HTTPStatusError):
logger.error(
"Cannot fetch lnbits status manifest."
f" {settings.lnbits_status_manifest}"
)
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
status_minutes = settings.lnbits_notification_server_status_hours * 60
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.
"""
while settings.lnbits_running:
funding_source = get_funding_source()
if (
settings.lnbits_watchdog
and funding_source.__class__.__name__ != "VoidWallet"
):
if settings.notification_balance_delta_changed:
try:
balance = await get_balance_delta()
delta = balance.delta_msats
logger.debug(f"Running watchdog task. current delta: {delta}")
if delta + settings.lnbits_watchdog_delta <= 0:
logger.error(f"Switching to VoidWallet. current delta: {delta}")
await switch_to_voidwallet()
except Exception as e:
logger.error("Error in watchdog task", e)
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
# runs by default every minute, the delta should not change that often
await check_balance_delta_changed()
except Exception as ex:
logger.error(ex)
if minute_counter % settings.lnbits_watchdog_interval_minutes == 0:
try:
await check_server_balance_against_node()
except Exception as ex:
logger.error(ex)
if minute_counter % status_minutes == 0:
try:
await _notify_server_status()
except Exception as ex:
logger.error(ex)
if minute_counter % 60 == 0:
try:
# initialize the list of all extensions
await InstallableExtension.get_installable_extensions()
except Exception as ex:
logger.error(ex)
minute_counter += 1
await asyncio.sleep(60)
async def _notify_server_status():
accounts = await get_accounts(filters=Filters(limit=0))
wallets_count = await get_wallets_count()
payments = await get_payments_status_count()
status = await get_balance_delta()
values = {
"up_time": settings.lnbits_server_up_time,
"accounts_count": accounts.total,
"wallets_count": wallets_count,
"in_payments_count": payments.incoming,
"out_payments_count": payments.outgoing,
"pending_payments_count": payments.pending,
"failed_payments_count": payments.failed,
"delta_sats": status.delta_sats,
"lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats,
}
enqueue_notification(NotificationType.server_status, values)
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
@@ -87,61 +100,6 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
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 push notification
await send_payment_push_notification(payment)
async def dispatch_webhook(payment: Payment):
"""
Dispatches the webhook to the webhook url.
"""
logger.debug("sending webhook", payment.webhook)
if not payment.webhook:
return await mark_webhook_sent(payment.payment_hash, -1)
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)
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):
wallet = await get_wallet(payment.wallet_id)
if wallet:
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
amount = int(payment.amount / 1000)
title = f"LNbits: {wallet.name}"
body = f"You just received {amount} sat{'s'[:amount^1]}!"
if payment.memo:
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}"
)
await send_push_notification(subscription, title, body, url)
async def wait_for_audit_data():
@@ -158,6 +116,16 @@ async def wait_for_audit_data():
await asyncio.sleep(3)
async def wait_notification_messages():
while settings.lnbits_running:
try:
await process_next_notification()
except Exception as ex:
logger.log("error", ex)
await asyncio.sleep(3)
async def purge_audit_data():
"""
Remove audit entries which have passed their retention period.
@@ -194,3 +162,14 @@ async def collect_exchange_rates_data():
else:
sleep_time = 60
await asyncio.sleep(sleep_time)
def _create_unique_task(name: str, func: Callable):
async def _to_coro(func: Callable[[], Coroutine]) -> Coroutine:
return await func()
try:
create_unique_task(name, _to_coro(func))
except Exception as e:
logger.error(f"Error in {name} task", e)
logger.error(traceback.format_exc())
+45 -29
View File
@@ -15,11 +15,8 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>Enable audit</q-item-label>
<q-item-label caption
>Record HTTP requests according with the specified
filters</q-item-label
>
<q-item-label v-text="$t('enable_audit')"></q-item-label>
<q-item-label caption v-text="$t('audit_desc')"></q-item-label>
</q-item-section>
</q-item>
</div>
@@ -48,14 +45,16 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>Record Request Body</q-item-label>
<q-item-label caption
>Warning:
<q-item-label v-text="$t('audit_record_req')"></q-item-label>
<q-item-label caption>
<span v-text="$t('audit_record_warning')"></span>
<br />
<ul>
<li>confidential data (like passwords) will be logged</li>
<li>the request body can have large size.</li>
<li><span v-text="$t('audit_record_req_warning_1')"></span></li>
<li><span v-text="$t('audit_record_req_warning_2')"></span></li>
</ul>
Use it with caution.
<br />
<span v-text="$t('audit_record_use')"></span>
</q-item-label>
</q-item-section>
</q-item>
@@ -72,8 +71,12 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>Record IP Address</q-item-label>
<q-item-label caption>Save the client IP address.</q-item-label>
<q-item-label>
<span v-text="$t('audit_ip')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('audit_ip_desc')"></span>
</q-item-label>
</q-item-section>
</q-item>
</div>
@@ -89,8 +92,12 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>Record Path Parameters</q-item-label>
<q-item-label caption>Recommended. </q-item-label>
<q-item-label>
<span v-text="$t('audit_path_params')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('recommended')"></span>
</q-item-label>
</q-item-section>
</q-item>
</div>
@@ -106,8 +113,12 @@
/>
</q-item-section>
<q-item-section>
<q-item-label>Record Query Parameters</q-item-label>
<q-item-label caption>Recommended.</q-item-label>
<q-item-label>
<span v-text="$t('audit_query_params')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('recommended')"></span>
</q-item-label>
</q-item-section>
</q-item>
</div>
@@ -115,25 +126,26 @@
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
<div class="row q-mb-lg">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p>Include HTTP Methods</p>
<p><span v-text="$t('audit_http_methods')"></span></p>
<q-select
filled
v-model="formData.lnbits_audit_http_methods"
multiple
hint="List of HTTP methods to be included. Empty lists means all."
label="HTTP Methods"
:hint="$t('audit_http_methods_hint')"
:label="$t('audit_http_methods_label')"
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']"
></q-select>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<p>Include HTTP Response Codes</p>
<p><span v-text="$t('audit_resp_codes')"></span></p>
<q-input
class="q-mb-md"
filled
v-model="formAddIncludeResponseCode"
@keydown.enter="addIncludeResponseCode"
type="text"
label="HTTP Response code (regex)"
hint="List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*"
:label="$t('audit_resp_codes_label')"
:hint="$t('audit_resp_codes_hint')"
>
<q-btn @click="addIncludeResponseCode" dense flat icon="add"></q-btn>
</q-input>
@@ -154,14 +166,15 @@
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p>Include Paths</p>
<p><span v-text="$t('audit_paths')"></span></p>
<q-input
class="q-mb-md"
filled
v-model="formAddIncludePath"
@keydown.enter="addIncludePath"
type="text"
label="HTTP Path (regex)"
hint="List of paths to be included (regex match). Empty list means all."
:label="$t('audit_paths_label')"
:hint="$t('audit_paths_hint')"
>
<q-btn @click="addIncludePath" dense flat icon="add"></q-btn>
</q-input>
@@ -180,14 +193,17 @@
<br />
</div>
<div class="col-md-6 col-sm-12">
<p>Exclude Paths</p>
<p>
<span v-text="$t('audit_paths_exclude')"></span>
</p>
<q-input
class="q-mb-md"
filled
v-model="formAddExcludePath"
@keydown.enter="addExcludePath"
type="text"
label="HTTP Path (regex)"
hint="List of paths to be excluded (regex match). Empty list means none."
:label="$t('audit_paths_exclude_label')"
:hint="$t('audit_paths_exclude_hint')"
>
<q-btn @click="addExcludePath" dense flat icon="add"></q-btn>
</q-input>
@@ -1,5 +1,7 @@
<q-tab-panel name="exchange_providers">
<h6 class="q-my-none q-mb-sm">Exchange Providers</h6>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('exchange_providers')"></span>
</h6>
<div class="row">
<div class="col-md-8 col-sm-12">
@@ -1,17 +1,22 @@
<q-tab-panel name="extensions">
<q-card-section class="q-pa-none">
<div>
<h6 class="q-my-none">Extensions</h6>
<h6 class="q-my-none">
<span v-text="$t('extensions')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12">
<p>Extension Sources</p>
<div class="col-12 q-mb-md">
<p>
<span v-text="$t('extension_sources')"></span>
</p>
<q-input
class="q-mb-md"
filled
v-model="formAddExtensionsManifest"
@keydown.enter="addExtensionsManifest"
type="text"
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
hint="Repositories from where the extensions can be downloaded"
:label="$t('ext_sources_label')"
:hint="$t('ext_sources_hint')"
>
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
</q-input>
@@ -30,34 +35,44 @@
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Admin Extensions</p>
<p>
<span v-text="$t('admin_extensions')"></span>
</p>
<q-select
filled
v-model="formData.lnbits_admin_extensions"
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:label="$t('admin_extensions_label')"
:hint="$t('admin_extensions_hint')"
:options="g.extensions"
></q-select>
</div>
<div class="col-12 col-md-6">
<p>User Default Extensions</p>
<p>
<span v-text="$t('user_default_extensions')"></span>
</p>
<q-select
filled
v-model="formData.lnbits_user_default_extensions"
multiple
hint="Extensions that will be enabled by default for the users."
label="User extensions"
:label="$t('user_default_extensions_label')"
:hint="$t('user_default_extensions_hint')"
:options="g.extensions"
></q-select>
</div>
<div class="col-12 col-md-6">
<p>Miscellaneous</p>
<p>
<span v-text="$t('miscellaneous')"></span>
</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Disable Extensions</q-item-label>
<q-item-label caption>Disables all extensions</q-item-label>
<q-item-label>
<span v-text="$t('misc_disable_extensions')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('misc_disable_extensions_label')"></span>
</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
@@ -71,10 +86,12 @@
</q-item>
<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-label>
<span v-text="$t('misc_hide_api')"></span>
</q-item-label>
<q-item-label caption>
<span v-text="$t('misc_hide_api_label')"></span>
</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
+140 -40
View File
@@ -1,88 +1,104 @@
<q-tab-panel name="funding">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">Wallets Management</h6>
<h6 class="q-my-none">
<span v-text="$t('wallets_management')"></span>
</h6>
<br />
<div>
<div class="row">
<div class="col">
<p>Funding Source Info</p>
<p>
<span v-text="$t('funding_source_info')"></span>
</p>
<ul>
<li
v-text="'Funding Source: '+ settings.lnbits_backend_wallet_class"
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
></li>
<li
v-text="'Node Balance: ' + (auditData.node_balance_msats /
1000).toLocaleString() + ' sats'"
v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})"
></li>
<li
v-text="'LNbits Balance: ' + (auditData.lnbits_balance_msats /
1000).toLocaleString() + ' sats'"
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})"
></li>
<li
v-text="'Reserve Percent: ' + (auditData.node_balance_msats /
auditData.lnbits_balance_msats * 100).toFixed(2) + ' %'"
v-text="$t('funding_reserve_percent', {
percent: auditData.lnbits_balance_sats > 0
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
: 100
})"
></li>
</ul>
<br />
</div>
<div class="col">
{% if LNBITS_NODE_UI_AVAILABLE %}
<p>Node Management</p>
<p><span v-text="$t('node_management')"></span></p>
<q-toggle
label="Node UI"
:label="$t('toggle_node_ui')"
v-model="formData.lnbits_node_ui"
></q-toggle>
<q-toggle
v-if="formData.lnbits_node_ui"
label="Public node UI"
:label="$t('toggle_public_node_ui')"
v-model="formData.lnbits_public_node_ui"
></q-toggle>
<br />
<q-toggle
v-if="formData.lnbits_node_ui"
label="Transactions Tab (Disable on large CLN nodes)"
:label="$t('toggle_transactions_node_ui')"
v-model="formData.lnbits_node_ui_transactions"
></q-toggle>
{% else %}
<p>Node Management not supported by active funding source</p>
<p><span v-text="$t('node_management_not_supported')"></span></p>
{% endif %}
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<p>Invoice Expiry</p>
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve')"></span></p>
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
:label="$t('fee_reserve_msats')"
>
</q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve_percent')"></span></p>
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
:label="$t('reserve_fee_in_percent')"
step="0.1"
></q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('invoice_expiry')"></span></p>
<q-input
filled
v-model.number="formData.lightning_invoice_expiry"
type="number"
label="Invoice expiry (seconds)"
:label="$t('invoice_expiry_label')"
mask="#######"
>
</q-input>
</div>
<div class="col-12 col-md-8">
<p>Fee reserve</p>
<div class="row q-col-gutter-md">
<div class="col-6">
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
label="Reserve fee in msats"
>
</q-input>
</div>
<div class="col-6">
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
label="Reserve fee in percent"
step="0.1"
></q-input>
</div>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('payment_wait_time')"></span></p>
<q-input
type="number"
filled
name="lnbits_funding_source_pay_invoice_wait_seconds"
v-model="formData.lnbits_funding_source_pay_invoice_wait_seconds"
:label="$t('payment_wait_time')"
:hint="$t('payment_wait_time_desc')"
step="1"
min="0"
></q-input>
</div>
</div>
<div v-if="isSuperUser">
@@ -91,6 +107,90 @@
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
</div>
<q-separator></q-separator>
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_watchdog_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_watchdog_switch_to_voidwallet"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_watchdog_limit')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_watchdog_limit_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_watchdog"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_interval_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_interval_minutes"
type="number"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_delta_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,67 @@
<q-tab-panel name="library">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('image_library')"></span>
</h6>
<q-btn
color="primary"
label="Add Image"
@click="$refs.imageInput.click()"
class="q-mb-md"
/>
<input
type="file"
ref="imageInput"
accept="image/png, image/jpeg, image/gif"
style="display: none"
@change="onImageInput"
/>
</q-card-section>
<div class="row q-col-gutter-sm q-pa-sm">
<div
v-for="image in library_images"
:key="image.filename"
class="col-6 col-sm-4 col-md-3 col-lg-2"
style="max-width: 200px"
>
<q-card class="q-mb-sm">
<q-img :src="image.url" style="height: 150px" />
<q-card-section
class="q-pt-md q-pb-md row items-center justify-between"
>
<small
><div
class="text-caption ellipsis"
style="max-width: 100px"
:title="image.filename"
v-text="image.filename"
></div
></small>
<q-btn
dense
flat
size="sm"
icon="content_copy"
@click="copyText(image.url)"
:title="$t('copy')"
><q-tooltip>Copy image link</q-tooltip></q-btn
>
<q-btn
dense
flat
size="sm"
icon="delete"
color="negative"
@click="deleteImage(image.filename)"
:title="$t('delete')"
><q-tooltip>Delete image</q-tooltip></q-btn
>
</q-card-section>
</q-card>
</div>
</div>
<div v-if="library_images.length === 0" class="q-pa-xl">
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
</div>
</q-tab-panel>
@@ -1,140 +1,505 @@
<q-tab-panel name="notifications">
<q-card-section class="q-pa-none">
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('enable_notifications')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_notifications_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notifications"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
<p
v-if="!formData.lnbits_notifications"
v-text="$t('notifications_disabled')"
></p>
<div v-if="formData.lnbits_notifications">
{% include "admin/_tab_security_notifications.html" %}
</div>
<br />
<div>
<p v-text="$t('notification_source')"></p>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('notifications_configure')"></span>
<q-btn
round
flat
@click="hideInputsToggle()"
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
></q-btn>
</h6>
<q-separator class="q-mt-md q-mb-sm"></q-separator>
<div class="row q-col-gutter-md">
<div class="col-sm-12 col-md-6">
<strong v-text="$t('notifications_nostr_config')"></strong>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_enable_nostr')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_enable_nostr_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_nostr_notifications_enabled"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_nostr_private_key')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_nostr_private_key_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_nostr_notifications_private_key"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_nostr_identifiers')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_nostr_identifiers_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_status_manifest"
type="text"
:label="$t('notification_source_label')"
v-model="nostrNotificationIdentifier"
@keydown.enter="addNostrNotificationIdentifier"
>
<q-btn
@click="addNostrNotificationIdentifier()"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="identifier in formData.lnbits_nostr_notifications_identifiers"
:key="identifier"
removable
@remove="removeNostrNotificationIdentifier(identifier)"
color="primary"
text-color="white"
><span class="ellipsis" v-text="identifier"></span
></q-chip>
</div>
</q-item-section>
</q-item>
</div>
<div class="col-sm-12 col-md-6">
<strong v-text="$t('notifications_telegram_config')"></strong>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_enable_telegram')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_enable_telegram_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_telegram_notifications_enabled"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_telegram_access_token')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_telegram_access_token_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_telegram_notifications_access_token"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('notifications_chat_id')"></q-item-label>
<q-item-label
caption
v-text="$t('notifications_chat_id_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_telegram_notifications_chat_id"
/>
</q-item-section>
</q-item>
</div>
<div class="col-sm-12">
<q-separator></q-separator>
</div>
<div class="col-12">
<strong v-text="$t('notifications_email_config')"></strong>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_enable_email')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_enable_email_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_email_notifications_enabled"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<div v-if="formData.lnbits_email_notifications_enabled" class="row">
<div class="col">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_email"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email_username')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_username_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_username"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email_password')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_password_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_password"
/>
</q-item-section>
</q-item>
<q-item>
<q-btn
@click="sendTestEmail()"
:label="$t('notifications_send_test_email')"
color="primary"
class="q-mt-md"
></q-btn>
</q-item>
</div>
<div class="col">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_to_emails')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_to_emails_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="emailNotificationAddress"
@keydown.enter="addEmailNotificationAddress"
>
<q-btn
@click="addEmailNotificationAddress()"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="to_email in formData.lnbits_email_notifications_to_emails"
:key="to_email"
removable
@remove="removeEmailNotificationAddress(to_email)"
color="primary"
text-color="white"
><span class="ellipsis" v-text="to_email"></span
></q-chip>
</div>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email_server_port')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_server_port_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_port"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email_server')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_server_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_server"
/>
</q-item-section>
</q-item>
</div>
<br />
</div>
<div class="col-12 col-md-6">
<p v-text="$t('killswitch')"></p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('enable_killswitch')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_killswitch_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_killswitch"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('killswitch_interval')"></q-item-label>
<q-item-label
caption
v-text="$t('killswitch_interval_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_killswitch_interval"
type="number"
/>
</q-item-section>
</q-item>
<br />
<p v-text="$t('watchdog')"></p>
<q-item disabled tag="label" v-ripple>
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
<q-item-section>
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_watchdog_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_watchdog"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<q-item disabled tag="label" v-ripple>
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
<q-item-section>
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_interval_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_interval"
type="number"
/>
</q-item-section>
</q-item>
<q-item disabled tag="label" v-ripple>
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
<q-item-section>
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_delta_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
type="number"
/>
</q-item-section>
</q-item>
<br />
</div>
</div>
</div>
<q-separator> </q-separator>
<h6 class="q-mb-sm">
<span v-text="$t('notifications')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_settings_update')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_settings_update_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_settings_update"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_credit_debit')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_credit_debit_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_credit_debit"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_server_start_stop')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_server_start_stop_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_server_start_stop"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_balance_delta_changed')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_balance_delta_changed_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.notification_balance_delta_changed"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_watchdog_limit')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_watchdog_limit_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_watchdog"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_server_status')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_server_status_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-input
class="flow-right"
type="number"
min="0"
filled
v-model="formData.lnbits_notification_server_status_hours"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_incoming_payment')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_incoming_payment_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-input
class="flow-right"
type="number"
min="0"
filled
v-model="formData.lnbits_notification_incoming_payment_amount_sats"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_outgoing_payment')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_outgoing_payment_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-input
class="flow-right"
type="number"
min="0"
filled
v-model="formData.lnbits_notification_outgoing_payment_amount_sats"
/>
</q-item-section>
</q-item>
</div>
</div>
</q-card-section>
</q-tab-panel>
+50 -42
View File
@@ -1,26 +1,28 @@
<q-tab-panel name="security">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">Server Management</h6>
<h6 class="q-my-none"><span v-text="$t('server_management')"></span></h6>
<div class="row">
<div class="col-md-6">
<p>Base URL</p>
<p><span v-text="$t('base_url')"></span></p>
<q-input
filled
v-model.number="formData.lnbits_baseurl"
label="Static/Base url for the server"
:label="$t('base_url_label')"
></q-input>
<br />
</div>
</div>
<h6 class="q-my-none q-mb-sm">Authentication</h6>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('authentication')"></span>
</h6>
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-6">
<q-input
filled
v-model="formData.auth_token_expire_minutes"
type="number"
label="Token expire minutes"
hint="Time in minutes until the token expires"
:label="$t('auth_token_expiry_label')"
:hint="$t('auth_token_expiry_hint')"
>
</q-input>
</div>
@@ -29,8 +31,8 @@
filled
v-model="formData.auth_allowed_methods"
multiple
hint="Allowed authorization methods"
label="Select authorization methods"
:hint="$t('auth_allowed_methods_hint')"
:label="$t('auth_allowed_methods_label')"
:options="formData.auth_all_methods"
></q-select>
</div>
@@ -50,8 +52,8 @@
v-model="nostrAcceptedUrl"
@keydown.enter="addNostrUrl"
type="text"
label="Nostr Request URL"
hint="Absolute URL that the clients will use to login."
:label="$t('auth_nostr_label')"
:hint="$t('auth_nostr_hint')"
>
<q-btn @click="addNostrUrl" dense flat icon="add"></q-btn>
</q-input>
@@ -81,8 +83,8 @@
<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"
:label="$t('auth_google_ci_label')"
:hint="$t('auth_google_ci_hint')"
>
</q-input>
</div>
@@ -91,7 +93,7 @@
filled
v-model="formData.google_client_secret"
type="password"
label="Google Client Secret"
:label="$t('auth_google_cs_label')"
>
</q-input>
</div>
@@ -108,8 +110,8 @@
<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"
:label="$t('auth_gh_client_id_label')"
:hint="$t('auth_gh_client_id_hint')"
>
</q-input>
</div>
@@ -118,7 +120,7 @@
filled
v-model="formData.github_client_secret"
type="password"
label="GitHub Client Secret"
:label="$t('auth_gh_client_secret_label')"
>
</q-input>
</div>
@@ -135,7 +137,7 @@
<q-input
filled
v-model="formData.keycloak_discovery_url"
label="Keycloak Discovey URL"
:label="$t('auth_keycloak_label')"
>
</q-input>
</div>
@@ -143,8 +145,8 @@
<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"
:label="$t('auth_keycloak_ci_label')"
:hint="$t('auth_keycloak_ci_hint')"
>
</q-input>
</div>
@@ -153,7 +155,7 @@
filled
v-model="formData.keycloak_client_secret"
type="password"
label="Keycloak Client Secret"
:label="$t('auth_keycloak_cs_label')"
>
</q-input>
</div>
@@ -281,31 +283,37 @@
</div>
<div class="col-12 col-md-12">
<p v-text="$t('wallet_limiter')"></p>
<p v-text="$t('callback_url_rules')"></p>
<div class="row q-col-gutter-md">
<div class="col-4">
<div class="col-12">
<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>
v-model="formCallbackUrlRule"
@keydown.enter="addCallbackUrlRule"
type="text"
:label="$t('enter_callback_url_rule')"
:hint="$t('callback_url_rule_hint')"
>
<q-btn
@click="addCallbackUrlRule"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="rule in formData.lnbits_callback_url_rules"
:key="rule"
removable
@remove="removeCallbackUrlRule(rule)"
color="primary"
text-color="white"
:label="rule"
class="ellipsis"
></q-chip>
</div>
<br />
</div>
</div>
</div>
@@ -1,77 +0,0 @@
<q-banner v-if="updateAvailable" class="bg-primary text-white">
<q-icon size="28px" name="update"></q-icon>
<span v-text="$t('update_available', {version: statusData.version})"></span>
<template v-slot:action>
<a
class="q-btn"
color="white"
target="_blank"
rel="noopener noreferrer"
href="https://github.com/lnbits/lnbits/releases"
v-text="$t('releases')"
></a>
</template>
</q-banner>
<q-banner v-if="!updateAvailable" class="bg-green text-white">
<q-icon size="28px" name="checkmark"></q-icon>
<span v-text="$t('latest_update', {version: statusData.version})"></span>
</q-banner>
<q-card>
<q-card-section>
<q-table
dense
flat
:rows="statusData.notifications"
:columns="statusDataTable.columns"
:no-data-label="$t('no_notifications')"
>
<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"
v-text="col.label"
></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width class="text-center">
<q-icon
v-if="props.row.type === 'update'"
size="18px"
name="update"
color="green"
></q-icon>
<q-icon
v-if="props.row.type === 'warning'"
size="18px"
name="warning"
color="red"
></q-icon>
</q-td>
<q-td
auto-width
key="date"
:props="props"
v-text="formatDate(props.row.date)"
>
</q-td>
<q-td key="message" :props="props"
><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
></q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
+91 -26
View File
@@ -1,79 +1,144 @@
<q-tab-panel name="server">
<q-card-section class="q-pa-none">
<div>
<h6 class="q-my-none">Currency Settings</h6>
<h6 class="q-my-none"><span v-text="$t('currency_settings')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Allowed currencies</p>
<p><span v-text="$t('allowed_currencies')"></span></p>
<q-select
filled
v-model="formData.lnbits_allowed_currencies"
multiple
hint="Limit the number of available fiat currencies"
label="Allowed currencies"
:hint="$t('allowed_currencies_hint')"
:label="$t('allowed_currencies')"
:options="{{ currencies | safe }}"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p>Default Accounting Currency</p>
<p><span v-text="$t('default_account_currency')"></span></p>
<q-select
filled
v-model="formData.lnbits_default_accounting_currency"
clearable
hint="Default currency for accounting"
label="Currency"
:hint="$t('default_account_currency_hint')"
:label="$t('currency')"
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
></q-select>
<br />
</div>
</div>
<br />
<h6 class="q-my-none">Service Fee</h6>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('payments')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p><span v-text="$t('max_outgoing_payment_amount')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_max_outgoing_payment_amount_sats"
:label="$t('max_outgoing_payment_amount')"
step="1"
min="0"
:hint="$t('max_outgoing_payment_amount_desc')"
></q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('max_incoming_payment_amount')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_max_incoming_payment_amount_sats"
:label="$t('max_incoming_payment_amount')"
step="1"
min="0"
:hint="$t('max_incoming_payment_amount_desc')"
></q-input>
</div>
<div class="col-12 col-md-6"></div>
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('wallet_limiter')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<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-12 col-md-3">
<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-12 col-md-3">
<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 class="col-12 col-md-3">
<q-toggle
v-model="formData.lnbits_only_allow_incoming_payments"
:label="$t('only_incoming_payments_allowed')"
><q-tooltip v-text="$t('disable_outgoing_payments')"></q-tooltip
></q-toggle>
</div>
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('service_fee')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Service Fee</p>
<p><span v-text="$t('service_fee')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee"
label="Service fee (%)"
:label="$t('service_fee_label')"
step="0.1"
hint="Fee charged per tx (%)"
:hint="$t('service_fee_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Service fee max</p>
<p><span v-text="$t('service_fee_max')"></span></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)"
:label="$t('service_fee_max_label')"
:hint="$t('service_fee_max_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Fee Wallet</p>
<p><span v-text="$t('fee_wallet')"></span></p>
<q-input
filled
v-model="formData.lnbits_service_fee_wallet"
label="Fee wallet (wallet ID)"
hint="Wallet ID to send funds to"
:label="$t('fee_wallet_label')"
:hint="$t('fee_wallet_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Disable Service Fee for Internal Payments</p>
<p><span v-text="$t('disable_fee_internal')"></span></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-label v-text="$t('disable_fee')"></q-item-label>
<q-item-label
caption
v-text="$t('disable_fee_internal_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
+89 -39
View File
@@ -1,82 +1,72 @@
<q-tab-panel name="site_customisation">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">UI Management</h6>
<h6 class="q-my-none"><span v-text="$t('ui_management')"></span></h6>
<br />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-5">
<p>Site Title</p>
<p><span v-text="$t('ui_site_title')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_title"
label="Site title"
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
></q-input>
<br />
</div>
<div class="col-12 col-md-5">
<p>Site Tagline</p>
<p><span v-text="$t('ui_site_tagline')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_tagline"
label="Site tagline"
:label="$t('ui_site_tagline')"
></q-input>
<br />
</div>
<div class="col-12 col-md-2 q-mt-xl">
<q-toggle
tip="Remove homepage elements like 'runs on' etc"
:tip="$t('ui_toggle_elements_tip')"
v-model="formData.lnbits_show_home_page_elements"
:label="formData.lnbits_show_home_page_elements ? 'Enable elements on homepage' : 'Disable elements on homepage'"
:label="formData.lnbits_show_home_page_elements ? $t('ui_elements_enable') : $t('ui_elements_disable')"
></q-toggle>
</div>
</div>
<div>
<p>Site Description</p>
<p><span v-text="$t('ui_site_description')"></span></p>
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
hint="Use plain text, Markdown, or raw HTML"
:hint="$t('ui_site_description_hint')"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<p>Default Wallet Name</p>
<p><span v-text="$t('ui_default_wallet_name')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_default_wallet_name"
label="LNbits wallet"
:label="$t('lnbits_wallet')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p>Denomination</p>
<q-input
filled
type="text"
v-model="formData.lnbits_denomination"
label="sats"
hint="The name for the FakeWallet token"
></q-input>
</div>
<div class="col-12 col-md-4">
<p>QR code logo</p>
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_qr_logo"
label="https://example.com/image.svg"
hint="URL to logo image in QR code"
:hint="$t('ui_qr_code_logo_hint')"
></q-input>
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<p>Custom Badge</p>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8">
<q-input
@@ -84,7 +74,7 @@
type="text"
tip="Custom Badge"
v-model.trim="formData.lnbits_custom_badge"
label="Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'"
:label="$t('ui_custom_badge_label')"
></q-input>
</div>
<div class="col-12 col-md-4">
@@ -92,67 +82,127 @@
filled
v-model="formData.lnbits_custom_badge_color"
:options="colors"
label="Custom badge color"
:label="$t('ui_custom_badge_color_label')"
></q-select>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_image')"></span></p>
<q-input
filled
type="text"
tip="Custom Image"
v-model.trim="formData.lnbits_custom_image"
:label="$t('ui_custom_image_label')"
:hint="$t('ui_custom_image_hint')"
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Themes</p>
<p><span v-text="$t('themes')"></span></p>
<q-select
filled
v-model="formData.lnbits_theme_options"
multiple
hint="Choose themes available for users"
:hint="$t('themes_hint')"
:options="lnbits_theme_options"
label="Themes"
:label="$t('themes')"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p>Custom Logo</p>
<p><span v-text="$t('custom_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_custom_logo"
label="https://example.com/image.png"
hint="URL to logo image"
:hint="$t('custom_logo_hint')"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Ad Space Title</p>
<p><span v-text="$t('ad_space_title')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
label="Supported by"
:label="$t('ad_space_title_label')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Advertisement Slots</p>
<p><span v-text="$t('ad_slots')"></span></p>
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
label="url;img_light_url;img_dark_url, url..."
hint="Ad url and image filepaths in CSV format, extensions can choose to honor"
:label="$t('ad_slots_label')"
:hint="$t('ad_slots_hint')"
>
</q-input>
<q-toggle
v-model="formData.lnbits_ad_space_enabled"
:label="formData.lnbits_ad_space_enabled ? 'Ads enabled' : 'Ads disabled'"
:label="formData.lnbits_ad_space_enabled ? $t('ads_enabled') : $t('ads_disabled')"
/>
<br />
</div>
</div>
<p><span v-text="$t('ui_default_theme')"></span></p>
<div class="row q-col-gutter-md">
<div class="col">
<q-select
v-model="formData.lnbits_default_border"
:options="globalBorderOptions"
label="Border"
@update:model-value="applyGlobalBorder"
>
</q-select>
</div>
<div class="col">
<q-select
v-model="formData.lnbits_default_theme"
:options="lnbits_theme_options"
label="Theme"
@update:model-value="applyGlobalTheme"
>
</q-select>
</div>
<div class="col">
<q-select
v-model="formData.lnbits_default_reaction"
:options="reactionOptions"
label="Payment reaction"
@update:model-value="applyGlobalReaction"
>
</q-select>
</div>
<div class="col">
<q-input
type="text"
v-model="formData.lnbits_default_bgimage"
label="Background Image"
@update:model-value="applyGlobalBgimage"
hint="This must be a trusted source. It can change the content and it can log your IP address."
>
</q-input>
</div>
<div class="col">
<q-toggle
type="bool"
v-model="formData.lnbits_default_gradient"
color="primary"
label="Gradient Background"
>
</q-toggle>
</div>
</div>
</div>
</q-card-section>
</q-tab-panel>
+14 -11
View File
@@ -1,17 +1,19 @@
<q-tab-panel name="users">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">User Management</h6>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('user_management')"></span>
</h6>
<div class="row">
<div class="col-12 col-md-6 q-pr-sm">
<p>Admin Users</p>
<p><span v-text="$t('admin_users')"></span></p>
<q-input
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
label="User ID"
hint="Users with admin privileges"
:label="$t('admin_users_label')"
:hint="$t('admin_users_hint')"
>
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
@@ -31,14 +33,14 @@
<br />
</div>
<div class="col-12 col-md-6">
<p>Allowed Users</p>
<p><span v-text="$t('allowed_users')"></span></p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
label="User ID"
hint="Only these users can use LNbits"
:label="$t('allowed_users_label')"
:hint="$t('allowed_users_hint')"
>
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
@@ -58,10 +60,11 @@
<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-label v-text="$t('allow_creation_user')"></q-item-label>
<q-item-label
caption
v-text="$t('allow_creation_user_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
+80 -57
View File
@@ -1,60 +1,75 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col q-mb-md">
<q-btn
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges">
<span v-text="$t('save_tooltip')"></span>
</q-tooltip>
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
{% endblock %} {% block page %}
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges">
<span v-text="$t('save_tooltip')"></span>
</q-tooltip>
<q-btn
v-if="isSuperUser"
:label="$t('restart')"
color="primary"
@click="restartServer"
class="q-ml-md"
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
:label="$t('restart')"
color="primary"
@click="restartServer"
class="q-ml-md"
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
flat
v-if="isSuperUser"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
<q-btn
:label="$t('download_backup')"
flat
@click="downloadBackup"
></q-btn>
<q-btn
flat
v-if="isSuperUser"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
</div>
<div></div>
</div>
</div>
</q-card>
</div>
</div>
@@ -134,6 +149,14 @@
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('audit')"></span></q-tooltip
></q-tab>
<q-tab
name="library"
icon="image"
:label="$q.screen.gt.sm ? $t('library') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('library')"></span></q-tooltip
></q-tab>
<q-tab
style="word-break: break-all"
name="site_customisation"
@@ -162,7 +185,8 @@
"admin/_tab_extensions.html" %} {% include
"admin/_tab_notifications.html" %} {% include
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
%}{% include "admin/_tab_audit.html"%}
%}{% include "admin/_tab_audit.html"%}{% include
"admin/_tab_library.html"%}
</q-tab-panels>
</q-form>
</template>
@@ -173,7 +197,9 @@
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>Create Currecny Ticker Converter</strong>
<div class="q-mb-md">
<strong v-text="$t('create_ticker_converter')"></strong>
</div>
<div class="row">
<div class="col-12 q-mb-md">
<q-select
@@ -211,7 +237,4 @@
</div>
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
{% endblock %}
+42 -14
View File
@@ -1,36 +1,63 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm q-pl-lg">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<!-- Optional: Add content here if needed -->
</div>
<div>
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
to="/admin#audit"
>
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
</q-btn>
</div>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center q-mb-lg">
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong>Components</strong>
<strong v-text="$t('components')"></strong>
<div style="width: 250px" class="q-pa-sm">
<canvas ref="componentUseChart"></canvas>
<canvas v-if="chartsReady" ref="componentUseChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong>To 5 Long Running Endpoints</strong>
<strong v-text="$t('long_running_endpoints')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas ref="longDurationChart"></canvas>
<canvas v-if="chartsReady" ref="longDurationChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong>HTTP Request Methods</strong>
<strong v-text="$t('http_request_methods')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas ref="requestMethodChart"></canvas>
<canvas v-if="chartsReady" ref="requestMethodChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong>HTTP Response Codes</strong>
<strong v-text="$t('http_response_codes')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas ref="responseCodeChart"></canvas>
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
</div>
</q-card>
</div>
@@ -70,6 +97,7 @@
/>
</template>
</q-input>
<q-select
v-else-if="['component', 'response_code','request_method'].includes(col.name)"
v-model="searchData[col.name]"
@@ -96,7 +124,9 @@
class="cursor-pointer q-mr-xs"
@click="showDetailsDialog(props.row)"
>
<q-tooltip>Request Details</q-tooltip>
<q-tooltip
><span v-text="$t('request_details')"></span
></q-tooltip>
</q-btn>
<span v-text="formatDate(props.row.created_at)"></span>
@@ -140,7 +170,7 @@
<q-dialog v-model="auditDetailsDialog.show" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>HTTP Request Details</strong>
<strong v-text="$t('http_request_details')"></strong>
<q-input
filled
dense
@@ -168,6 +198,4 @@
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/audit.js') }}"></script>
{% endblock %}
+57 -8
View File
@@ -85,7 +85,12 @@
<q-card-section>
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
@@ -96,7 +101,8 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
<i v-text="wallet.inkey"></i>"</code
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
>"</code
>
</q-card-section>
</q-card>
@@ -112,7 +118,12 @@
<q-card-section>
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;,
@@ -130,8 +141,9 @@
<code
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
'{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;}' -H
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
application/json"</code
"X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
"Content-type: application/json"</code
>
</q-card-section>
</q-card>
@@ -218,7 +230,12 @@
/api/v1/payments/&lt;payment_hash&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "{{ wallet.inkey }}"}</code>
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
@@ -227,10 +244,42 @@
<code
>curl -X GET
<span v-text="baseUrl"></span>api/v1/payments/&lt;payment_hash&gt; -H
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
application/json"</code
"X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
"Content-type: application/json"</code
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<code
><span class="text-pink">WS</span>
/api/v1/ws/&lt;invoice_key&gt;</code
>
<h5
class="text-caption q-mt-sm q-mb-none"
v-text="$t('websocket_example')"
></h5>
<code
>wscat -c <span v-text="websocketUrl"></span>/<span
v-text="inkeyHidden ? '****************' : wallet.inkey"
></span
></code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)/payments
</h5>
<code>{"balance": &lt;int&gt;, "payment": &lt;object&gt;}</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-card-section>
<p v-text="$t('reset_wallet_keys_desc')"></p>
<q-btn
unelevated
color="red-10"
@click="resetKeys()"
:label="$t('reset_wallet_keys')"
></q-btn>
</q-card-section>
</q-expansion-item>
+423 -40
View File
@@ -1,39 +1,51 @@
{% extends "base.html" %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% 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 %}
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div>
<div class="q-gutter-y-md">
<q-tabs v-model="tab" align="left">
<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-tab
name="api_acls"
:label="$t('access_control_list')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md">
<div v-if="user" class="col-12 col-md-6 q-gutter-y-md">
<div v-if="user" class="col-md-12 col-lg-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="credentialsData.show">
<q-separator></q-separator>
<q-card-section>
<div class="row">
<div class="col">
<h4 class="q-my-none">
<span v-text="$t('password_config')"></span>
<span v-text="$t('password')"></span>
</h4>
</div>
<div class="col">
@@ -90,7 +102,7 @@
unelevated
color="primary"
class="float-right"
:label="$t('change_password')"
:label="$t('update_password')"
>
</q-btn>
</q-card-section>
@@ -98,7 +110,7 @@
<q-card-section>
<div class="col q-mb-sm">
<h4 class="q-my-none">
<span v-text="$t('pubkey')"></span>
Nostr <span v-text="$t('pubkey')"></span>
</h4>
</div>
<q-input
@@ -129,11 +141,10 @@
</q-card-section>
</div>
<div v-else>
<q-card-section>
<q-card-section v-if="user.extra.picture">
<div class="row">
<div class="col">
<q-img
v-if="user.extra.picture"
style="max-width: 100px"
:src="user.extra.picture"
class="float-right"
@@ -141,7 +152,6 @@
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-input
@@ -277,7 +287,7 @@
</q-btn>
<q-btn
@click="showUpdateCredentials()"
:label="$t('update_credentials')"
:label="$t('change_password')"
filled
color="primary"
class="float-right"
@@ -286,6 +296,15 @@
</div>
</q-tab-panel>
<q-tab-panel name="theme">
<q-btn
v-if="g.user.admin"
class="absolute-top-right"
flat
round
icon="settings"
to="/admin#site_customisation"
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
></q-btn>
<div class="row q-mb-md">
<div class="col-4"><span v-text="$t('language')"></span></div>
<div class="col-8">
@@ -325,7 +344,7 @@
v-if="g.allowedThemes.includes('classic')"
dense
flat
@click="changeColor('classic')"
@click="themeChoiceFunc('classic')"
icon="circle"
color="deep-purple"
size="md"
@@ -335,9 +354,9 @@
v-if="g.allowedThemes.includes('bitcoin')"
dense
flat
@click="changeColor('bitcoin')"
@click="themeChoiceFunc('bitcoin')"
icon="circle"
color="orange"
color="deep-orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
@@ -345,7 +364,7 @@
v-if="g.allowedThemes.includes('mint')"
dense
flat
@click="changeColor('mint')"
@click="themeChoiceFunc('mint')"
icon="circle"
color="green"
size="md"
@@ -354,7 +373,7 @@
v-if="g.allowedThemes.includes('autumn')"
dense
flat
@click="changeColor('autumn')"
@click="themeChoiceFunc('autumn')"
icon="circle"
color="brown"
size="md"
@@ -364,7 +383,7 @@
v-if="g.allowedThemes.includes('monochrome')"
dense
flat
@click="changeColor('monochrome')"
@click="themeChoiceFunc('monochrome')"
icon="circle"
color="grey"
size="md"
@@ -374,7 +393,7 @@
v-if="g.allowedThemes.includes('salvador')"
dense
flat
@click="changeColor('salvador')"
@click="themeChoiceFunc('salvador')"
icon="circle"
color="blue-10"
size="md"
@@ -384,7 +403,7 @@
v-if="g.allowedThemes.includes('freedom')"
dense
flat
@click="changeColor('freedom')"
@click="themeChoiceFunc('freedom')"
icon="circle"
color="pink-13"
size="md"
@@ -394,7 +413,7 @@
v-if="g.allowedThemes.includes('cyber')"
dense
flat
@click="changeColor('cyber')"
@click="themeChoiceFunc('cyber')"
icon="circle"
color="light-green-9"
size="md"
@@ -404,7 +423,7 @@
v-if="g.allowedThemes.includes('flamingo')"
dense
flat
@click="changeColor('flamingo')"
@click="themeChoiceFunc('flamingo')"
icon="circle"
color="pink-3"
size="md"
@@ -412,6 +431,22 @@
</q-btn>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('background_image')"></span>
</div>
<div class="col-8">
<q-input
v-model="bgimageSelection"
:label="$t('background_image')"
@update:model-value="bgimageChoiceFunc"
>
<q-tooltip
><span v-text="$t('background_image')"></span
></q-tooltip>
</q-input>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('gradient_background')"></span>
@@ -458,9 +493,9 @@
</div>
<div class="col-8">
<q-select
v-model="borderChoice"
v-model="borderSelection"
:options="borderOptions"
label="Reactions"
label="Borders"
@update:model-value="applyBorder"
>
<q-tooltip
@@ -470,7 +505,9 @@
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">Notifications</div>
<div class="col-4">
<span v-text="$t('notifications')"></span>
</div>
<div class="col-8">
<lnbits-notifications-btn
v-if="g.user"
@@ -484,7 +521,7 @@
</div>
<div class="col-8">
<q-select
v-model="reactionChoice"
v-model="reactionSelection"
:options="reactionOptions"
label="Reactions"
@update:model-value="reactionChoiceFunc"
@@ -496,6 +533,230 @@
</div>
</div>
</q-tab-panel>
<q-tab-panel name="api_acls">
<div class="row q-mb-md">
<q-badge v-if="user.admin">
<span
v-text="$t('access_control_list_admin_warning')"
></span>
</q-badge>
</div>
<q-card-section>
<div class="row q-mb-md q-gutter-y-md">
<div class="col-sm-12 col-md-6">
<q-select
v-model="selectedApiAcl.id"
emit-value
map-options
@update:model-value="handleApiACLSelected"
:options="apiAcl.data.map(t => ({label: t.name, value: t.id}))"
:label="$t('access_control_list')"
dense
>
</q-select>
</div>
<div class="col-sm-12 col-md-6">
<q-btn
@click="askPasswordAndRunFunction('newApiAclDialog')"
filled
outline
icon="add"
:label="$t('access_control_list')"
color="grey"
class="float-right"
></q-btn>
</div>
</div>
<div v-if="selectedApiAcl.id">
<div class="row q-mb-md">
<div class="col-sm-12 col-md-6">
<q-select
:options="selectedApiAcl.token_id_list.map(t => ({label: t.name, value: t.id}))"
v-model="apiAcl.selectedTokenId"
emit-value
map-options
:label="$t('api_tokens')"
dense
>
</q-select>
</div>
<div class="col-sm-12 col-md-6 q-pl-sm">
<q-btn
v-if="apiAcl.selectedTokenId"
@click="askPasswordAndRunFunction('deleteToken')"
icon="delete"
filled
color="negative"
class="float-left"
></q-btn>
<q-btn
@click="askPasswordAndRunFunction('newTokenAclDialog')"
outline
icon="add"
:label="$t('api_token')"
filled
color="grey"
class="float-right"
></q-btn>
</div>
</div>
<div v-if="apiAcl.apiToken" class="row q-mb-md">
<div class="col-12">
<q-badge>
<span>Use this token in the HTTP</span>
<strong>
&nbsp;<code>Authorization</code>
&nbsp;
</strong>
<span> header.</span>
</q-badge>
</div>
<div class="col-12">
<table
class="full-width lnbits__table-bordered"
style="
border-collapse: collapse;
background-color: grey;
"
>
<thead>
<tr>
<th>
<span class="float-left">Header Name</span>
</th>
<th>
<span class="float-left">Header Value</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>Authorization</strong>
</td>
<td>
<div class="row q-pt-sm">
<div class="col-2 q-mt-sm">
<strong>Bearer &nbsp;</strong>
</div>
<div class="col-10">
<q-input
v-model="apiAcl.apiToken"
:label="$t('api_token_id')"
filled
dense
readonly
:type="selectedApiAcl.showId ? 'text': 'password'"
class="q-mb-md"
>
<q-btn
@click="selectedApiAcl.showId = !selectedApiAcl.showId"
dense
flat
:icon="selectedApiAcl.showId ? 'visibility_off' : 'visibility'"
color="black"
></q-btn>
<q-btn
@click="copyText(apiAcl.apiToken)"
icon="content_copy"
color="black"
flat
dense
></q-btn>
</q-input>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-12">
<q-badge>
<span
>Please store this token. It cannot be later
retrieved, only revoked.</span
>
</q-badge>
</div>
</div>
<q-table
row-key="path"
:rows="selectedApiAcl.endpoints"
:columns="apiAcl.columns"
v-model:pagination="apiAcl.pagination"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<q-toggle
v-if="col.name == 'read'"
v-model="selectedApiAcl.allRead"
@update:model-value="handleAllEndpointsReadAccess()"
:label="$t('read')"
size="sm"
></q-toggle>
<q-toggle
v-else-if="col.name == 'write'"
v-model="selectedApiAcl.allWrite"
@update:model-value="handleAllEndpointsWriteAccess()"
:label="$t('write')"
size="sm"
></q-toggle>
<span v-else v-text="col.label"></span>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td>
<span v-text="props.row.name"></span>
</q-td>
<q-td>
<span v-text="props.row.path"></span>
</q-td>
<q-td>
<q-toggle size="sm" v-model="props.row.read" />
</q-td>
<q-td>
<q-toggle size="sm" v-model="props.row.write" />
</q-td>
</q-tr>
</template>
</q-table>
<q-separator></q-separator>
</div>
<div v-if="selectedApiAcl.id" class="row q-mt-md">
<div class="col-sm-12 col-md-6">
<q-btn
@click="askPasswordAndRunFunction('updateApiACLs')"
:label="$t('update')"
filled
color="primary"
></q-btn>
</div>
<div class="col-sm-12 col-md-6">
<q-btn
@click="askPasswordAndRunFunction('deleteApiACL')"
:label="$t('delete')"
icon="delete"
color="negative"
class="float-right"
>
</q-btn>
</div>
</div>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
</div>
</div>
@@ -510,4 +771,126 @@
</q-card>
</div>
</div>
<q-dialog v-model="apiAcl.showPasswordDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>User Password</strong>
<div class="row q-mt-md q-col-gutter-md">
<div class="col-12">
<q-input
v-model="apiAcl.password"
type="password"
dense
filled
label="Password"
hint="User password is required for this action."
>
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
@click="runPasswordGuardedFunction()"
:label="$t('ok')"
color="primary"
></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="apiAcl.showNewAclDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>New API Access Control List</strong>
<div class="row q-mt-md q-col-gutter-md">
<div class="col-12">
<q-input v-model="apiAcl.newAclName" dense filled label="ACL Name">
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn @click="addApiACL()" label="Create" color="primary"></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="apiAcl.showNewTokenDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>New API Token</strong>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12">
<q-input v-model="apiAcl.newTokenName" dense filled label="Token Name">
</q-input>
</div>
<div class="col-12">
<q-input
v-model="apiAcl.newTokenExpiry"
dense
filled
label="Expiration"
hit="Expiration time in minutes (default xxx)"
>
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
>
<q-date v-model="apiAcl.newTokenExpiry" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
<template v-slot:append>
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
>
<q-time
v-model="apiAcl.newTokenExpiry"
mask="YYYY-MM-DD HH:mm"
format24h
>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn @click="generateApiToken()" label="Create" color="primary"></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
{% endblock %}
+32 -685
View File
@@ -1,10 +1,14 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {{ window_vars(user, extensions) }}{% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user, wallet, extensions, extension_data)
}}{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-xs">
<div>
<div class="q-gutter-y-md">
<q-tabs
:model-value="tab"
@@ -49,6 +53,14 @@
v-text="$t('new_version') + ` (${updatableExtensions?.length})`"
></span>
</q-badge>
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
to="/admin#extensions"
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
></q-btn>
</q-tabs>
</div>
</div>
@@ -57,6 +69,7 @@
</div>
<div class="row q-col-gutter-md">
<div
v-if="filteredExtensions"
class="col-12 col-sm-6 col-md-6 col-lg-4"
v-for="extension in filteredExtensions"
:key="extension.id + extension.hash"
@@ -105,6 +118,7 @@
></q-tooltip>
</q-badge>
<div
v-if="extension.name"
class="text-h5"
style="cursor: pointer"
@click="showExtensionDetails(extension.id, extension.details_link)"
@@ -177,7 +191,7 @@
<div class="col-10">
<div v-if="!extension.inProgress">
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
v-if="g.user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="primary"
type="a"
@@ -185,20 +199,20 @@
:label="$t('open')"
></q-btn>
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
v-if="g.user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="grey-5"
@click="disableExtension(extension)"
:label="$t('disable')"
></q-btn>
<q-badge
v-if="extension.isAdminOnly && !user.admin"
v-if="extension.isAdminOnly && !g.user.admin"
v-text="$t('admin_only')"
>
</q-badge>
<q-btn
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
v-else-if="extension.isInstalled && extension.isActive && !g.user.extensions.includes(extension.id)"
flat
color="primary"
@click="enableExtensionForUser(extension)"
@@ -221,7 +235,7 @@
</q-btn>
</div>
<div v-else>
<q-spinner color="primary" size="2.55em"></q-spinner>
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
</div>
</div>
@@ -323,7 +337,7 @@
</a>
</div>
<div v-else>
<q-spinner color="primary" size="2.55em"></q-spinner>
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
</div>
</q-card-section>
@@ -422,7 +436,10 @@
:header-class="getReleaseIconColor(release)"
>
<div v-if="release.inProgress">
<q-spinner color="primary" size="2.55em"></q-spinner>
<q-spinner-bars
color="primary"
size="2.55em"
></q-spinner-bars>
</div>
<div v-else-if="release.error">
<q-icon
@@ -433,7 +450,7 @@
></q-icon>
<span v-text="$t('release_details_error')"></span>
</div>
<q-card v-else>
<q-card class="no-border" v-else>
<q-card-section v-if="release.is_version_compatible">
<span
v-if="release.requiresPayment && !release.paid_sats"
@@ -608,7 +625,7 @@
></q-btn>
</div>
</div>
<q-spinner v-else color="primary" size="2.55em"></q-spinner>
<q-spinner-bars v-else color="primary" size="2.55em"></q-spinner-bars>
<div
v-if="selectedExtension"
v-show="manageExtensionTab === 'sell'"
@@ -694,7 +711,7 @@
</a>
</div>
<div v-else class="col">
<q-spinner color="primary" size="2.55em"></q-spinner>
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
</div>
</div>
<div class="row q-mt-lg">
@@ -942,12 +959,12 @@
value="false"
:label="item.name + ` (v${item.latestRelease?.version})`"
>
<q-spinner
<q-spinner-bars
v-if="item.inProgress"
color="primary"
size="1.5em"
class="q-ml-md"
></q-spinner>
></q-spinner-bars>
</q-checkbox>
</div>
</div>
@@ -971,674 +988,4 @@
</div>
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
window.app = Vue.createApp({
el: '#vue',
data: function () {
return {
slide: 0,
fullscreen: false,
autoplay: true,
searchTerm: '',
tab: 'all',
manageExtensionTab: 'releases',
filteredExtensions: null,
updatableExtensions: [],
showUninstallDialog: false,
showManageExtensionDialog: false,
showExtensionDetailsDialog: false,
showDropDbDialog: false,
showPayToEnableDialog: false,
showUpdateAllDialog: false,
dropDbExtensionId: '',
selectedExtension: null,
selectedImage: null,
selectedExtensionDetails: null,
selectedExtensionRepos: null,
selectedRelease: null,
uninstallAndDropDb: false,
maxStars: 5,
paylinkWebsocket: null,
user: null
}
},
watch: {
searchTerm(term) {
this.filterExtensions(term, this.tab)
}
},
methods: {
handleTabChanged: function (tab) {
this.filterExtensions(this.searchTerm, tab)
},
filterExtensions: function (term, tab) {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
?.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.extensions
.filter(e => (tab === 'all' ? !e.isInstalled : true))
.filter(e => (tab === 'installed' ? e.isInstalled : true))
.filter(e =>
tab === 'installed'
? e.isActive
? true
: !!this.g.user.admin
: true
)
.filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(extensionNameContains(term))
.map(e => ({
...e,
details_link:
e.installedRelease?.details_link || e.latestRelease?.details_link
}))
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.showManageExtensionDialog = false
release.payment_hash =
release.payment_hash || this.getPaylinkHash(release.pay_link)
LNbits.api
.request(
'POST',
`/api/v1/extension`,
this.g.user.wallets[0].adminkey,
{
ext_id: extension.id,
archive: release.archive,
source_repo: release.source_repo,
payment_hash: release.payment_hash,
version: release.version
}
)
.then(response => {
extension.isAvailable = true
extension.isInstalled = true
extension.installedRelease = release
this.toggleExtension(extension)
extension.inProgress = false
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
window.location.reload()
})
.catch(err => {
console.warn(err)
extension.inProgress = false
LNbits.utils.notifyApiError(err)
})
},
uninstallExtension: async function () {
const extension = this.selectedExtension
this.showManageExtensionDialog = false
this.showUninstallDialog = false
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.isAvailable = false
extension.isInstalled = false
extension.inProgress = false
extension.installedRelease = null
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
Quasar.Notify.create({
type: 'positive',
message: 'Extension uninstalled!'
})
if (this.uninstallAndDropDb) {
this.showDropDb()
} else {
setTimeout(() => {
window.location.reload()
}, 300)
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
dropExtensionDb: async function () {
const extension = this.selectedExtension
this.showManageExtensionDialog = false
this.showDropDbDialog = false
this.dropDbExtensionId = ''
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}/db`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.installedRelease = null
extension.inProgress = false
extension.hasDatabaseTables = false
Quasar.Notify.create({
type: 'positive',
message: 'Extension DB deleted!'
})
setTimeout(() => {
window.location.reload()
}, 300)
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
toggleExtension(extension) {
const action = extension.isActive ? 'activate' : 'deactivate'
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/${action}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
timeout: 2000,
type: 'positive',
message: `Extension '${extension.id}' ${action}d!`
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.isActive = false
extension.inProgress = false
})
},
enableExtensionForUser: function (extension) {
if (extension.isPaymentRequired) {
this.showPayToEnable(extension)
return
}
this.enableExtension(extension)
},
enableExtension: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/enable`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Extension enabled!'
})
setTimeout(() => {
window.location.reload()
}, 300)
})
.catch(err => {
console.warn(err)
LNbits.utils.notifyApiError(err)
})
},
disableExtension: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/disable`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Extension disabled!'
})
setTimeout(() => {
window.location.reload()
}, 300)
})
.catch(err => {
console.warn(error)
LNbits.utils.notifyApiError(err)
})
},
showPayToEnable: function (extension) {
this.selectedExtension = extension
this.selectedExtension.payToEnable.paidAmount =
extension.payToEnable.amount
this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true
},
updatePayToInstallData: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/sell`,
this.g.user.wallets[0].adminkey,
{
required: extension.payToEnable.required,
amount: extension.payToEnable.amount,
wallet: extension.payToEnable.wallet
}
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Payment info updated!'
})
this.showManageExtensionDialog = false
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
showUninstall: function () {
this.showManageExtensionDialog = false
this.showUninstallDialog = true
this.uninstallAndDropDb = false
},
showDropDb: function () {
this.showDropDbDialog = true
},
showManageExtension: async function (extension) {
this.selectedExtension = extension
this.selectedRelease = null
this.selectedExtensionRepos = null
this.manageExtensionTab = 'releases'
this.showManageExtensionDialog = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extension.id}/releases`,
this.g.user.wallets[0].adminkey
)
this.selectedExtensionRepos = data.reduce((repos, release) => {
repos[release.source_repo] = repos[release.source_repo] || {
releases: [],
isInstalled: false,
repo: release.repo
}
release.inProgress = false
release.error = null
release.loaded = false
release.isInstalled = this.isInstalledVersion(
this.selectedExtension,
release
)
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
}, {})
} catch (error) {
LNbits.utils.notifyApiError(error)
extension.inProgress = false
}
},
showExtensionDetails: async function (extId, detailsLink) {
if (!detailsLink) {
return
}
this.selectedExtensionDetails = null
this.showExtensionDetailsDialog = true
this.slide = 0
this.fullscreen = false
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
this.g.user.wallets[0].inkey
)
this.selectedExtensionDetails = data
this.selectedExtensionDetails.description_md =
LNbits.utils.convertMarkdown(data.description_md)
} catch (error) {
console.warn(error)
}
},
async payAndInstall(release) {
try {
this.selectedExtension.inProgress = true
this.showManageExtensionDialog = false
const paymentInfo = await this.requestPaymentForInstall(
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 payAndEnable(extension) {
try {
const paymentInfo = await this.requestPaymentForEnable(
extension.id,
extension.payToEnable.paidAmount
)
const wallet = this.g.user.wallets.find(
w => w.id === extension.payToEnable.paymentWallet
)
const {data} = await LNbits.api.payInvoice(
wallet,
paymentInfo.payment_request
)
this.enableExtension(extension)
this.showPayToEnableDialog = false
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async showInstallQRCode(release) {
this.selectedRelease = release
try {
const data = await this.requestPaymentForInstall(
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 showEnableQRCode(extension) {
try {
extension.payToEnable.showQRCode = true
this.selectedExtension = _.clone(extension)
const data = await this.requestPaymentForEnable(
extension.id,
extension.payToEnable.paidAmount
)
extension.payToEnable.paymentRequest = data.payment_request
this.selectedExtension = _.clone(extension)
const url = new URL(window.location)
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
url.pathname = `/api/v1/ws/${data.payment_hash}`
const ws = new WebSocket(url)
ws.addEventListener('message', async ({data}) => {
const payment = JSON.parse(data)
if (payment.pending === false) {
Quasar.Notify.create({
type: 'positive',
message: 'Invoice Paid!'
})
this.enableExtension(extension)
ws.close()
}
})
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async requestPaymentForInstall(extId, release) {
const {data} = await LNbits.api.request(
'PUT',
`/api/v1/extension/${extId}/invoice/install`,
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
},
async requestPaymentForEnable(extId, amount) {
const {data} = await LNbits.api.request(
'PUT',
`/api/v1/extension/${extId}/invoice/enable`,
this.g.user.wallets[0].adminkey,
{
amount
}
)
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) {
Quasar.Notify.create({
type: 'positive',
message: 'Invoice Paid!'
})
this.installExtension(this.selectedRelease)
} else {
Quasar.Notify.create({
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 (
extension.installedRelease.version !==
extension.latestRelease.version
)
}
},
isInstalledVersion: function (extension, release) {
if (extension.installedRelease) {
return (
extension.installedRelease.source_repo === release.source_repo &&
extension.installedRelease.version === release.version
)
}
},
getReleaseIcon: function (release) {
if (!release.is_version_compatible) return 'block'
if (release.isInstalled) return 'download_done'
return 'download'
},
getReleaseIconColor: function (release) {
if (!release.is_version_compatible) return 'text-red'
if (release.isInstalled) return 'text-green'
return ''
},
getGitHubReleaseDetails: async function (release) {
if (!release.is_github_release || release.loaded) {
return
}
const [org, repo] = release.source_repo.split('/')
release.inProgress = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
this.g.user.wallets[0].adminkey
)
release.loaded = true
release.is_version_compatible = data.is_version_compatible
release.min_lnbits_version = data.min_lnbits_version
release.warning = data.warning
} catch (error) {
console.warn(error)
release.error = error
LNbits.utils.notifyApiError(error)
} finally {
release.inProgress = false
}
},
selectAllUpdatableExtensionss: async function () {
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
},
updateSelectedExtensions: async function () {
let count = 0
for (const ext of this.updatableExtensions) {
try {
if (!ext.selectedForUpdate) {
continue
}
ext.inProgress = true
await LNbits.api.request(
'POST',
`/api/v1/extension`,
this.g.user.wallets[0].adminkey,
{
ext_id: ext.id,
archive: ext.latestRelease.archive,
source_repo: ext.latestRelease.source_repo,
payment_hash: ext.latestRelease.payment_hash,
version: ext.latestRelease.version
}
)
count++
ext.isAvailable = true
ext.isInstalled = true
ext.isUpgraded = true
ext.inProgress = false
ext.installedRelease = ext.latestRelease
this.toggleExtension(ext)
} catch (err) {
console.warn(err)
Quasar.Notify.create({
type: 'negative',
message: `Failed to update ${ext.code}!`
})
} finally {
ext.inProgress = false
}
}
Quasar.Notify.create({
type: 'positive',
message: `${count} extensions updated!`
})
this.showUpdateAllDialog = false
setTimeout(() => {
window.location.reload()
}, 2000)
}
},
created: function () {
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
...e,
inProgress: false,
selectedForUpdate: false
}))
this.filteredExtensions = this.extensions.concat([])
for (let i = 0; i < this.filteredExtensions.length; i++) {
if (this.filteredExtensions[i].isInstalled != false) {
this.handleTabChanged('installed')
this.tab = 'installed'
}
}
if (window.user) {
this.user = LNbits.map.user(window.user)
}
this.updatableExtensions = this.extensions.filter(ext =>
this.hasNewVersion(ext)
)
},
mixins: [windowMixin]
})
</script>
{% endblock %}
+144 -133
View File
@@ -1,139 +1,150 @@
{% 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;
}
{% extends "public.html" %} {% block page_container %}
<q-page-container>
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
{% 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 v-text="$t('welcome_lnbits')"></strong>
<p><span v-text="$t('setup_su_account')"></span></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 || $t('invalid_password_repeat')]"
><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>
window.app = Vue.createApp({
el: '#vue',
mixins: [window.windowMixin],
data: function () {
return {
loginData: {
isPwd: true,
isPwdRepeat: true,
username: '',
password: '',
passwordRepeat: ''
@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;
}
}
},
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
})
</style>
<script>
window.app = Vue.createApp({
el: '#vue',
mixins: [window.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)
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
}
}
}
})
</script>
})
</script>
{% endblock %}
</q-page>
</q-page-container>
{% endblock %}
File diff suppressed because it is too large Load Diff
+544 -219
View File
@@ -1,14 +1,9 @@
{% extends "base.html" %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user, wallet) }}
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
{% endblock %}
<!---->
{% block title %}{{ wallet_name }} - {{ SITE_TITLE }} {% endblock %}
<!---->
{% block page %}
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
%}
<div class="row q-col-gutter-md">
{% if HIDE_API and AD_SPACE %}
<div class="col-12 col-md-8 q-gutter-y-md">
@@ -25,102 +20,131 @@
} : ''"
>
{% endif %}
<q-scroll-area
v-if="!mobileSimple"
style="
height: 115px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
"
>
<div class="row no-wrap q-gutter-md q-pr-md">
<q-card
v-for="wallet in g.user.wallets"
:key="wallet.id"
class="wallet-list-card"
bordered
tag="a"
:href="wallet.url"
:style="
g.wallet && g.wallet.id === wallet.id
? `border: 1px solid ${primaryColor}; width: 250px; text-decoration: none;`
: 'width: 250px; text-decoration: none;'
"
:class="{
'active-wallet-card': g.wallet && g.wallet.id === wallet.id
}"
>
<q-card-section>
<div class="row items-center">
<q-avatar
size="lg"
:color="
g.wallet && g.wallet.id === wallet.id
? $q.dark.isActive
? 'primary'
: 'primary'
: 'grey-5'
"
>
<q-icon
name="flash_on"
:size="$q.dark.isActive ? '21px' : '20px'"
:color="$q.dark.isActive ? 'black' : 'grey-3'"
></q-icon>
</q-avatar>
<div
class="text-h6 q-pl-md"
:class="{
'text-bold': g.wallet && g.wallet.id === wallet.id
}"
v-text="wallet.name"
></div>
</div>
<div class="row items-center q-pt-sm">
<h6 class="q-my-none text-no-wrap">
<strong v-text="wallet.fsat"></strong>
<small> {{LNBITS_DENOMINATION}}</small>
</h6>
</div>
</q-card-section>
</q-card>
</div>
</q-scroll-area>
<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': ''
} : ''"
, boxShadow: $q.screen.lt.md ? 'none !important': ''
, border: $q.screen.lt.md ? 'none !important': ''
, width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
} : ''"
>
<q-card-section>
<h3 class="q-my-none text-no-wrap">
<strong v-text="formattedBalance"></strong>
<small> {{LNBITS_DENOMINATION}}</small>
<lnbits-update-balance
:wallet_id="this.g.wallet.id"
@credit-value="handleBalanceUpdate"
class="q-ml-md"
></lnbits-update-balance>
</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>
<q-card-section style="height: 130px">
<div class="row q-gutter-md">
<div
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
class="col-1"
style="max-width: 30px"
>
<q-btn
v-if="g.fiatTracking"
@click="swapBalancePriority"
style="height: 50px"
class="q-mt-lg"
color="primary"
flat
dense
icon="swap_vert"
></q-btn>
</div>
<div class="col">
<div
class="column"
:class="{
'q-pt-sm': g.fiatTracking,
'q-pt-lg': !g.fiatTracking
}"
v-if="!isFiatPriority || !g.fiatTracking"
style="height: 100px"
>
<div class="col-7">
<div class="row">
<div class="col-auto">
<div class="text-h3 q-my-none full-width">
<strong
v-text="walletFormatBalance(this.g.wallet.sat)"
class="text-no-wrap"
:style="{fontSize: 'clamp(0.75rem, 10vw, 3rem)', display: 'inline-block', maxWidth: '100%'}"
></strong>
</div>
</div>
<div class="col-auto">
<lnbits-update-balance
v-if="$q.screen.lt.lg"
:wallet_id="this.g.wallet.id"
:callback="updateBalanceCallback"
:small_btn="true"
></lnbits-update-balance>
</div>
</div>
</div>
<div class="col-2">
<div v-if="g.fiatTracking">
<span
class="text-h5 text-italic"
v-text="formattedFiatAmount"
style="opacity: 0.75"
></span>
</div>
</div>
</div>
<div
class="column"
v-if="isFiatPriority && g.fiatTracking"
:class="{
'q-pt-sm': g.fiatTracking,
'q-pt-lg': !g.fiatTracking
}"
style="height: 100px"
>
<div class="col-7">
<div class="row">
<div class="col-auto">
<div
v-if="g.fiatTracking"
class="text-h3 q-my-none text-no-wrap"
>
<strong v-text="formattedFiatAmount"></strong>
</div>
</div>
<div class="col-auto">
<lnbits-update-balance
v-if="$q.screen.lt.lg"
:wallet_id="this.g.wallet.id"
:callback="updateBalanceCallback"
:small_btn="true"
></lnbits-update-balance>
</div>
</div>
</div>
<div class="col-2">
<span
class="text-h5 text-italic"
style="opacity: 0.75"
v-text="formattedBalance + ' {{LNBITS_DENOMINATION}}'"
>
</span>
</div>
</div>
<div
class="absolute-right q-pa-md"
v-if="$q.screen.gt.md && g.fiatTracking && '{{ LNBITS_DENOMINATION }}' == 'sats'"
>
<div class="text-bold text-italic">BTC Price</div>
<span
class="text-bold text-italic"
v-text="formattedExchange"
></span>
</div>
<q-btn
@click="mobileSimple = !mobileSimple"
v-if="$q.screen.lt.md"
@click="simpleMobile()"
color="primary"
class="float-right lt-md"
class="q-ml-xl absolute-right"
dense
size="sm"
style="height: 20px; margin-top: 75px"
flat
:icon="mobileSimple ? 'unfold_more' : 'unfold_less'"
:label="mobileSimple ? $t('more') : $t('less')"
@@ -133,41 +157,44 @@
<q-btn
unelevated
color="primary"
class="full-width"
class="q-mr-md"
@click="showParseDialog"
:label="$t('paste_request')"
></q-btn>
</div>
<div class="col">
<q-btn
unelevated
color="primary"
class="full-width"
class="q-mr-md"
@click="showReceiveDialog"
:label="$t('create_invoice')"
></q-btn>
</div>
<div class="col">
<q-btn
unelevated
color="secondary"
icon="photo_camera"
icon="qr_code_scanner"
@click="showCamera"
:label="$t('scan')"
>
<q-tooltip
><span v-text="$t('camera_tooltip')"></span
></q-tooltip>
</q-btn>
<lnbits-update-balance
v-if="$q.screen.gt.md"
:wallet_id="this.g.wallet.id"
:callback="updateBalanceCallback"
:small_btn="false"
></lnbits-update-balance>
</div>
</div>
</q-card>
<q-card
:style="
$q.screen.lt.md
? {
background: $q.screen.lt.md ? 'none !important' : '',
boxShadow: $q.screen.lt.md ? 'none !important' : '',
border: $q.screen.lt.md ? 'none !important': '',
marginTop: $q.screen.lt.md ? '0px !important' : ''
}
: ''
@@ -175,26 +202,47 @@
>
<q-card-section>
<payment-list
@filter-changed="handleFilterChange"
:update="updatePayments"
:wallet="this.g.wallet"
:mobile-simple="mobileSimple"
/>
:expand-details="expandDetails"
></payment-list>
</q-card-section>
</q-card>
<div id="hiddenQrCodeContainer" style="display: none">
<lnbits-qrcode
:value="'lightning:' + this.receive.paymentReq"
></lnbits-qrcode>
</div>
</div>
{% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md">
{% else %}
<div
v-if="!mobileSimple || $q.screen.gt.sm"
class="col-12 col-md-5 q-gutter-y-md"
>
<div v-if="!mobileSimple" 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>
</h6>
<q-card-section class="q-pb-xs">
<div class="row items-center">
<q-avatar
size="lg"
:icon="g.wallet.extra.icon"
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
:color="g.wallet.extra.color"
>
</q-avatar>
<q-btn
@click="icon.show = true"
round
color="grey-5"
text-color="black"
size="xs"
icon="edit"
style="position: relative; left: -15px; bottom: -10px"
></q-btn>
<div class="text-subtitle1 q-mt-none q-mb-none">
{{ SITE_TITLE }} Wallet:
<strong><em v-text="g.wallet.name"></em></strong>
</div>
</div>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
@@ -221,99 +269,196 @@
{% endif %}
<q-expansion-item
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
group="extras"
icon="settings_cell"
:label="$t('export_to_phone')"
>
<q-card>
<q-card-section class="text-center">
<p v-text="$t('export_to_phone_desc')"></p>
<lnbits-qrcode :value="exportUrl"></lnbits-qrcode>
</q-card-section>
<span v-text="exportWalletQR"></span>
<q-card-actions class="flex-center q-pb-md">
<q-btn
outline
color="grey"
:label="$t('copy_wallet_url')"
@click="copyText(exportUrl)"
></q-btn>
</q-card-actions>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="edit"
:label="$t('rename_wallet')"
icon="phone_android"
:label="$t('access_wallet_on_mobile')"
>
<q-card>
<q-card-section>
<div class="" style="max-width: 320px">
<q-input
filled
v-model.trim="update.name"
label="Name"
dense
/>
</div>
<q-btn
:disable="!update.name.length"
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_name')"
@click="updateWallet({ name: update.name })"
></q-btn>
You can connect to this wallet from a mobile app:
<ul>
<li>
Download
<a class="text-secondary" href="https://zeusln.app"
>Zeus</a
>
or
<a
class="text-secondary"
href="https://bluewallet.io/"
>BlueWallet</a
>
from App Store or Google Play
</li>
<li>
Enable the
<a class="text-secondary" href="/lndhub">LndHub </a>
extension for this account
</li>
<li>
Scan the QR code in the
<a class="text-secondary" href="/lndhub">LndHub </a>
extensions with your mobile app
</li>
</ul>
</q-card-section>
<q-card-section>
Or you can access the wallet directly from your mobile
browser using:
<q-expansion-item
icon="mobile_friendly"
:label="$t('export_to_phone')"
>
<q-card>
<q-card-section class="text-center">
<p v-text="$t('export_to_phone_desc')"></p>
<lnbits-qrcode
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
></lnbits-qrcode>
</q-card-section>
<span v-text="exportWalletQR"></span>
<q-card-actions class="flex-center q-pb-md">
<q-btn
outline
color="grey"
:label="$t('copy_wallet_url')"
@click="copyText(`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`)"
></q-btn>
</q-card-actions>
</q-card>
</q-expansion-item>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="attach_money"
:label="$t('fiat_tracking')"
icon="settings"
:label="$t('wallet_config')"
>
<q-card>
<q-card-section>
<div style="max-width: 360px">
<q-select
filled
dense
clearable
v-model="update.currency"
type="text"
:label="$t('currency')"
:options="receive.units.filter((u) => u !== 'sat')"
></q-select>
<div class="row">
<div class="col-6">
<q-input
filled
v-model.trim="update.name"
label="Name"
dense
/>
</div>
<div class="col-4 q-pl-sm">
<q-btn
:disable="!update.name.length"
unelevated
class="q-mt-xs full-width"
color="primary"
:label="$t('update_name')"
dense
@click="updateWallet({ name: update.name })"
></q-btn>
</div>
<div class="col-2"></div>
</div>
</q-card-section>
<q-card-section
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
>
<div class="row">
<div class="col-6">
<q-select
filled
dense
v-model="update.currency"
type="text"
:disable="g.fiatTracking"
:options="receive.units.filter((u) => u !== 'sat')"
:label="$t('currency_settings')"
></q-select>
</div>
<div class="col-4 q-pl-sm">
<q-btn
dense
color="primary"
class="q-mt-xs full-width"
@click="handleFiatTracking()"
:disable="update.currency == ''"
:label="g.fiatTracking ? 'Remove' : 'Add'"
></q-btn>
</div>
<div class="col-2">
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
class="float-right q-mb-lg"
to="/admin#exchange_providers"
><q-tooltip
v-text="$t('exchange_providers')"
></q-tooltip
></q-btn>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="row">
<div class="col-6">
<p v-text="$t('delete_wallet_desc')"></p>
</div>
<div class="col-4 q-pl-sm">
<q-btn
unelevated
color="red-10"
class="full-width"
@click="deleteWallet()"
:label="$t('delete_wallet')"
></q-btn>
</div>
<div class="col-2"></div>
</div>
<q-btn
:disable="!update.name.length"
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_currency')"
@click="updateWallet({ currency: update.currency || '' })"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="charts"
icon="insights"
:label="$t('wallet_charts')"
>
<q-card>
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartConfig.showBalance"
:label="$t('payments_balance_chart')"
>
</q-checkbox>
</div>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="remove_circle"
:label="$t('delete_wallet')"
>
<q-card>
<q-card-section>
<p v-text="$t('delete_wallet_desc')"></p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet()"
:label="$t('delete_wallet')"
></q-btn>
<div class="col-md-4 col-sm-12">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartConfig.showBalanceInOut"
:label="$t('payments_balance_in_out_chart')"
>
</q-checkbox>
</div>
<div class="col-md-4 col-sm-12">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartConfig.showPaymentCountInOut"
:label="$t('payments_count_in_out_chart')"
>
</q-checkbox>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -337,7 +482,6 @@
style="display: inline-block"
href="{{ AD[0] }}"
class="q-ml-md q-mb-xs q-mr-md"
style="max-width: 80%"
>
<img
style="max-width: 100%; height: auto"
@@ -353,9 +497,84 @@
>{% endfor %}
</q-card>
{% endif %}
<div
v-show="chartDataPointCount"
class="col-12 col-md-5 q-gutter-y-md"
>
<q-card v-if="chartConfig.showBalance">
<q-card-section class="q-pa-none">
<div style="height: 200px" class="q-pa-sm">
<canvas ref="walletBalanceChart"></canvas>
</div>
</q-card-section>
</q-card>
<q-card v-if="chartConfig.showBalanceInOut">
<q-card-section class="q-pa-none">
<div style="height: 200px" class="q-pa-sm">
<canvas ref="walletBalanceInOut"></canvas>
</div>
</q-card-section>
</q-card>
<q-card v-if="chartConfig.showPaymentCountInOut">
<q-card-section class="q-pa-none">
<div style="height: 200px" class="q-pa-sm">
<canvas ref="walletPaymentsInOut"></canvas>
</div>
</q-card-section>
</q-card>
</div>
<div v-if="hasChartActive && !chartDataPointCount">
<q-card>
<q-card-section> No chart data available</q-card-section>
</q-card>
</div>
</div>
</div>
<q-dialog v-model="icon.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="setIcon" class="q-gutter-md">
<div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center">
<!-- Loop through all icons -->
<q-btn
v-for="(thisIcon, index) in icon.options"
:key="index"
@click="setSelectedIcon(thisIcon)"
round
text-color="black"
:color="icon.data.icon === thisIcon ? icon.data.color || 'primary' : 'grey-5'"
size="md"
:icon="thisIcon"
class="q-mb-sm"
></q-btn>
</div>
<div class="q-pa-sm flex justify-between items-center">
<div class="flex q-pl-lg">
<!-- Color options -->
<q-btn
v-for="(color, index) in icon.colorOptions"
:key="'color-' + index"
@click="setSelectedColor(color)"
round
:color="color"
size="xs"
style="width: 24px; height: 24px; min-width: 24px; padding: 0"
class="q-mr-xs"
></q-btn>
</div>
<q-btn
unelevated
color="primary"
:disable="!icon.data.icon"
type="submit"
>
Save Icon
</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog
v-model="receive.show"
position="top"
@@ -373,7 +592,7 @@
<q-input
filled
dense
v-model.number="receive.data.amount"
v-model="receive.data.amount"
:label="$t('amount')+' ({{LNBITS_DENOMINATION}}) *'"
mask="#.##"
fill-mask="0"
@@ -432,11 +651,11 @@
:label="$t('cancel')"
></q-btn>
</div>
<q-spinner
<q-spinner-bars
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner>
></q-spinner-bars>
</q-form>
</q-card>
<q-card
@@ -445,9 +664,7 @@
>
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<lnbits-qrcode
:value="'lightning:' + receive.paymentReq.toUpperCase()"
></lnbits-qrcode>
<div v-html="invoiceQrCode"></div>
</a>
</div>
<div class="text-center">
@@ -463,9 +680,13 @@
color="positive"
text-color="white"
></q-avatar>
NFC supported
<span v-text="$t('nfc_supported')"></span>
</q-chip>
<span v-else class="text-caption text-grey">NFC not supported</span>
<span
v-else
class="text-caption text-grey"
v-text="$t('nfc_not_supported')"
></span>
</div>
<div class="row q-mt-lg">
<q-btn
@@ -488,27 +709,131 @@
<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"
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>
<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>
<div class="column content-center text-center q-mb-md">
<div v-if="!isFiatPriority">
<h4 class="q-my-none text-bold">
<span v-text="walletFormatBalance(parse.invoice.sat)"></span>
</h4>
</div>
<div v-else>
<h4
class="q-my-none text-bold"
v-text="parse.invoice.fiatAmount"
></h4>
</div>
<div class="q-my-md absolute">
<q-btn
v-if="g.fiatTracking"
@click="swapBalancePriority"
flat
dense
icon="swap_vert"
color="primary"
></q-btn>
</div>
<div v-if="g.fiatTracking">
<div v-if="isFiatPriority">
<h5 class="q-my-none text-bold">
<span
v-text="walletFormatBalance(parse.invoice.sat)"
></span>
</h5>
</div>
<div v-else style="opacity: 0.75">
<div class="text-h5 text-italic">
<span
v-text="parse.invoice.fiatAmount"
style="opacity: 0.75"
></span>
</div>
</div>
</div>
</div>
<q-separator></q-separator>
<h6 class="text-center" v-text="parse.invoice.description"></h6>
<q-list separator bordered dense class="q-mb-md">
<q-expansion-item expand-separator icon="info" label="Details">
<q-list separator>
<q-item>
<q-item-section>
<q-item-label v-text="$t('created')"></q-item-label>
<q-item-label
caption
v-text="parse.invoice.createdDate"
></q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label
caption
v-text="parse.invoice.createdDateFrom"
></q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label v-text="$t('expire_date')"></q-item-label>
<q-item-label
caption
v-text="parse.invoice.expireDate"
></q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label
caption
v-text="parse.invoice.expireDateFrom"
></q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label v-text="$t('payment_hash')"></q-item-label>
<q-item-label
caption
v-text="`${parse.invoice.hash.slice(0, 12)}...${parse.invoice.hash.slice(-12)}`"
></q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>
<q-icon
name="content_copy"
@click="copyText(parse.invoice.hash)"
size="1em"
color="grey"
class="cursor-pointer"
/>
</q-item-label>
<q-tooltip>
<span v-text="parse.invoice.hash"></span>
</q-tooltip>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label v-text="$t('Invoice')"></q-item-label>
<q-item-label
caption
v-text="`${parse.invoice.bolt11.slice(0, 12)}...${parse.invoice.bolt11.slice(-12)}`"
></q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>
<q-icon
name="content_copy"
@click="copyText(parse.invoice.bolt11)"
size="1em"
color="grey"
class="cursor-pointer"
/>
</q-item-label>
<q-tooltip>
<span v-text="parse.invoice.bolt11"></span>
</q-tooltip>
</q-item-section>
</q-item>
</q-list>
</q-expansion-item>
</q-list>
<div v-if="canPay" class="row q-mt-lg">
<q-btn
unelevated
+76 -5
View File
@@ -29,7 +29,46 @@
</q-card>
</q-dialog>
<q-dialog v-model="openChannelDialog.show" position="top">
<q-dialog v-model="setFeeDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<label class="text-h6">Set Channel Fee</label>
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
<q-separator></q-separator>
<q-form class="q-gutter-md">
<q-input
dense
type="number"
filled
v-model.number="setFeeDialog.data.fee_ppm"
label="Fee Rate PPM"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="setFeeDialog.data.fee_base_msat"
label="Fee Base msat"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('set')"
color="primary"
@click="setChannelFee(setFeeDialog.channel_id)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="openChannelDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
@@ -171,10 +210,41 @@
<q-tr :props="props">
<div class="q-pb-sm">
<div class="row items-center q-gutter-sm">
<div
class="text-subtitle1 col-grow"
v-text="props.row.name"
></div>
<div class="text-subtitle1" v-text="props.row.name"></div>
<div class="text-caption" v-if="props.row.peer_id">
<span>Peer ID</span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.peer_id)"
></q-btn>
</div>
<div class="text-caption col-grow">
<span>Fees</span>
<q-btn
size="xs"
flat
dense
icon="settings"
@click="showSetFeeDialog(props.row.id)"
></q-btn>
<span v-if="props.row.fee_ppm">
<span v-text="props.row.fee_ppm"></span> ppm,
<span v-text="props.row.fee_base_msat"></span> msat
</span>
</div>
<div class="text-caption" v-if="props.row.id">
<span>Channel ID</span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.id)"
></q-btn>
</div>
<div class="text-caption" v-if="props.row.short_id">
<span v-text="props.row.short_id"></span>
<q-btn
@@ -201,6 +271,7 @@
color="pink"
></q-btn>
</div>
<lnbits-channel-balance
:balance="props.row.balance"
:color="props.row.color"
+5 -334
View File
@@ -1,5 +1,8 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<q-dialog v-model="nodeInfoDialog.show" position="top">
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
@@ -40,336 +43,4 @@
</q-card>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
window.app = Vue.createApp({
el: '#vue',
config: {
globalProperties: {
LNbits,
msg: 'hello'
}
},
mixins: [window.windowMixin],
data: function () {
return {
isSuperUser: false,
wallet: {},
tab: 'dashboard',
payments: 1000,
info: {},
channel_stats: {},
channels: {
data: [],
filter: ''
},
activeBalance: {},
ranks: {},
peers: {
data: [],
filter: ''
},
connectPeerDialog: {
show: false,
data: {}
},
openChannelDialog: {
show: false,
data: {}
},
closeChannelDialog: {
show: false,
data: {}
},
nodeInfoDialog: {
show: false,
data: {}
},
transactionDetailsDialog: {
show: false,
data: {}
},
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
],
stateFilters: [
{label: 'Active', value: 'active'},
{label: 'Pending', value: 'pending'}
],
paymentsTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'date',
align: 'left',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
},
{
name: 'fee',
align: 'right',
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
},
{
name: 'destination',
align: 'right',
label: 'Destination',
field: 'destination'
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
},
invoiceTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'paid_at',
field: 'paid_at',
align: 'right',
label: 'Paid at',
sortable: true
},
{
name: 'expiry',
label: this.$t('expiry'),
field: 'expiry',
align: 'right',
sortable: true
},
{
name: 'amount',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
}
}
},
created: function () {
this.getInfo()
this.get1MLStats()
},
watch: {
tab: function (val) {
if (val === 'transactions' && !this.paymentsTable.data.length) {
this.getPayments()
this.getInvoices()
} else if (val === 'channels' && !this.channels.data.length) {
this.getChannels()
this.getPeers()
}
}
},
computed: {
checkChanges() {
return !_.isEqual(this.settings, this.formData)
},
filteredChannels: function () {
return this.stateFilters
? this.channels.data.filter(channel => {
return this.stateFilters.find(({value}) => value == channel.state)
})
: this.channels.data
},
totalBalance: function () {
return this.filteredChannels.reduce(
(balance, channel) => {
balance.local_msat += channel.balance.local_msat
balance.remote_msat += channel.balance.remote_msat
balance.total_msat += channel.balance.total_msat
return balance
},
{local_msat: 0, remote_msat: 0, total_msat: 0}
)
}
},
methods: {
formatMsat: function (msat) {
return LNbits.utils.formatMsat(msat)
},
api: function (method, url, options) {
const params = new URLSearchParams(options?.query)
return LNbits.api
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
getChannels: function () {
return this.api('GET', '/channels').then(response => {
this.channels.data = response.data
})
},
getInfo: function () {
return this.api('GET', '/info').then(response => {
this.info = response.data
this.channel_stats = response.data.channel_stats
})
},
get1MLStats: function () {
return this.api('GET', '/rank').then(response => {
this.ranks = response.data
})
},
getPayments: function (props) {
if (props) {
this.paymentsTable.pagination = props.pagination
}
let pagination = this.paymentsTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/payments', {query}).then(response => {
this.paymentsTable.data = response.data.data
this.paymentsTable.pagination.rowsNumber = response.data.total
})
},
getInvoices: function (props) {
if (props) {
this.invoiceTable.pagination = props.pagination
}
let pagination = this.invoiceTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/invoices', {query}).then(response => {
this.invoiceTable.data = response.data.data
this.invoiceTable.pagination.rowsNumber = response.data.total
})
},
getPeers: function () {
return this.api('GET', '/peers').then(response => {
this.peers.data = response.data
console.log('peers', this.peers)
})
},
connectPeer: function () {
console.log('peer', this.connectPeerDialog)
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
() => {
this.connectPeerDialog.show = false
this.getPeers()
}
)
},
disconnectPeer: function (id) {
LNbits.utils
.confirmDialog('Do you really wanna disconnect this peer?')
.onOk(() => {
this.api('DELETE', `/peers/${id}`).then(response => {
Quasar.Notify.create({
message: 'Disconnected',
icon: null
})
this.needsRestart = true
this.getPeers()
})
})
},
openChannel: function () {
this.api('POST', '/channels', {data: this.openChannelDialog.data})
.then(response => {
this.openChannelDialog.show = false
this.getChannels()
})
.catch(error => {
console.log(error)
})
},
showCloseChannelDialog: function (channel) {
this.closeChannelDialog.show = true
this.closeChannelDialog.data = {
force: false,
short_id: channel.short_id,
...channel.point
}
},
closeChannel: function () {
this.api('DELETE', '/channels', {
query: this.closeChannelDialog.data
}).then(response => {
this.closeChannelDialog.show = false
this.getChannels()
})
},
showOpenChannelDialog: function (peer_id) {
this.openChannelDialog.show = true
this.openChannelDialog.data = {peer_id, funding_amount: 0}
},
showNodeInfoDialog: function (node) {
this.nodeInfoDialog.show = true
this.nodeInfoDialog.data = node
},
showTransactionDetailsDialog: function (details) {
this.transactionDetailsDialog.show = true
this.transactionDetailsDialog.data = details
console.log('details', details)
},
shortenNodeId(nodeId) {
return nodeId
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
: '...'
}
}
})
</script>
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
{% endblock %}
+390
View File
@@ -0,0 +1,390 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm q-pl-lg">
<div class="row items-center justify-between q-gutter-xs">
<div class="col"></div>
<div class="float-left">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartData.showPaymentStatus"
:label="$t('payments_status_chart')"
>
</q-checkbox>
</div>
<q-separator vertical class="q-ma-sm"></q-separator>
<div class="float-left">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartData.showPaymentTags"
:label="$t('payments_tag_chart')"
>
</q-checkbox>
</div>
<q-separator vertical class="q-ma-sm"></q-separator>
<div class="float-left">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartData.showBalance"
:label="$t('payments_balance_chart')"
>
</q-checkbox>
</div>
<q-separator vertical class="q-ma-sm"></q-separator>
<div class="float-left">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartData.showWalletsSize"
:label="$t('payments_wallets_chart')"
>
</q-checkbox>
</div>
<q-separator vertical class="q-ma-sm"></q-separator>
<div class="float-left">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartData.showBalanceInOut"
:label="$t('payments_balance_in_out_chart')"
>
</q-checkbox>
</div>
<q-separator vertical class="q-ma-sm"></q-separator>
<div class="float-left">
<q-checkbox
dense
@click="saveChartsPreferences"
v-model="chartData.showPaymentCountInOut"
:label="$t('payments_count_in_out_chart')"
>
</q-checkbox>
</div>
<q-separator vertical class="q-ma-sm"></q-separator>
<q-btn icon="event" outline flat>
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
>
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
<div class="row">
<div class="col-6">
<q-btn
label="Search"
@click="searchByDate()"
color="primary"
flat
class="float-left"
v-close-popup
/>
</div>
<div class="col-6">
<q-btn
v-close-popup
@click="clearDateSeach()"
label="Clear"
class="float-right"
color="grey"
flat
/>
</div>
</div>
</q-popup-proxy>
<q-badge
v-if="searchDate?.to || searchDate?.from"
class="q-mt-lg q-mr-md"
color="primary"
rounded
floating
style="border-radius: 6px"
/>
</q-btn>
<q-separator vertical class="q-ma-sm"></q-separator>
<div>
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
to="/admin#server"
>
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
</q-btn>
</div>
</div>
</div>
</q-card>
</div>
</div>
<div v-show="!showDetails">
<div class="row q-col-gutter-md justify-center q-mb-md">
<div
v-show="chartData.showPaymentStatus"
class="col-lg-3 col-md-6 col-sm-12 text-center"
>
<q-card class="q-pt-sm">
<strong v-text="$t('payment_chart_status')"></strong>
<div style="height: 300px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="paymentsStatusChart"></canvas>
</div>
</q-card>
</div>
<div
v-show="chartData.showPaymentStatus"
class="col-lg-3 col-md-6 col-sm-12 text-center"
>
<q-card class="q-pt-sm">
<strong v-text="$t('payment_chart_tags')"></strong>
<div style="height: 300px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="paymentsTagsChart"></canvas>
</div>
</q-card>
</div>
<div
v-show="chartData.showBalance"
class="col-lg-6 col-md-12 col-sm-12 text-center"
>
<q-card class="q-pt-sm">
<strong
v-text="$t('lnbits_balance', {balance: (lnbitsBalance || 0).toLocaleString()})"
></strong>
<div style="height: 300px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="paymentsDailyChart"></canvas>
</div>
</q-card>
</div>
<div
v-show="chartData.showWalletsSize"
class="col-lg-6 col-md-12 col-sm-12 text-center"
>
<q-card class="q-pt-sm">
<strong v-text="$t('payment_chart_tx_per_wallet')"></strong>
<div style="height: 300px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="paymentsWalletsChart"></canvas>
</div>
</q-card>
</div>
<div
v-show="chartData.showBalanceInOut"
class="col-lg-6 col-md-12 col-sm-12 text-center"
>
<q-card class="q-pt-sm">
<strong v-text="$t('payments_balance_in_out')"></strong>
<div style="height: 300px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="paymentsBalanceInOutChart"></canvas>
</div>
</q-card>
</div>
<div
v-show="chartData.showPaymentCountInOut"
class="col-lg-6 col-md-12 col-sm-12 text-center"
>
<q-card class="q-pt-sm">
<strong v-text="$t('payments_count_in_out')"></strong>
<div style="height: 300px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="paymentsCountInOutChart"></canvas>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col">
<q-card class="q-pa-md">
<q-table
row-key="payment_hash"
:rows="payments"
:columns="paymentsTable.columns"
v-model:pagination="paymentsTable.pagination"
:filter="paymentsTable.search"
@request="fetchPayments"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
<q-input
v-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
v-model="searchData[col.name]"
@keydown.enter="searchPaymentsBy()"
@update:model-value="searchPaymentsBy()"
dense
type="text"
filled
clearable
:label="col.label"
>
<template v-slot:append>
<q-icon
name="search"
@click="searchPaymentsBy()"
class="cursor-pointer"
/>
</template>
</q-input>
<q-select
v-else-if="['status'].includes(col.name)"
v-model="searchData[col.name]"
:options="searchOptions[col.name]"
@update:model-value="searchPaymentsBy()"
:label="col.label"
clearable
style="width: 100px"
></q-select>
<q-select
v-else-if="['tag'].includes(col.name)"
v-model="searchData[col.name]"
:options="searchOptions[col.name]"
@update:model-value="searchPaymentsBy()"
:label="col.label"
clearable
style="width: 100px"
></q-select>
<span v-else v-text="col.label"></span>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr auto-width :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.name == 'status'">
<q-tooltip
><span v-text="$t('payment_details')"></span
></q-tooltip>
<q-icon
@click="showDetailsToggle(props.row)"
v-if="props.row.status === 'success'"
size="14px"
:name="props.row.outgoing ? 'call_made' : 'call_received'"
:color="props.row.outgoing ? 'pink' : 'green'"
class="cursor-pointer"
></q-icon>
<q-icon
v-else-if="props.row.status === 'pending'"
@click="showDetailsToggle(props.row)"
name="downloading"
:style="props.row.outgoing ? 'transform: rotate(225deg)' : 'transform: scaleX(-1) rotate(315deg)'"
color="grey"
class="cursor-pointer"
></q-icon>
<q-icon
v-else
@click="showDetailsToggle(props.row)"
name="warning"
color="yellow"
class="cursor-pointer"
></q-icon>
</div>
<div v-else-if="col.name == 'created_at'">
<div>
<q-tooltip anchor="top middle">
<span v-text="formatDate(props.row.created_at)"></span>
</q-tooltip>
<span v-text="props.row.timeFrom"> </span>
</div>
</div>
<div
v-else-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
>
<q-btn
v-if="props.row[col.name]"
icon="content_copy"
size="sm"
flat
class="cursor-pointer q-mr-xs"
@click="copyText(props.row[col.name])"
>
<q-tooltip anchor="top middle">Copy</q-tooltip>
</q-btn>
<span v-text="shortify(props.row[col.name], col.max_length)">
</span>
<q-tooltip>
<span v-text="props.row[col.name]"></span>
</q-tooltip>
</div>
<span
v-else
v-text="props.row[col.name]"
class="cursor-pointer"
></span>
</q-td>
</q-tr>
</template>
</q-table>
</q-card>
</div>
</div>
</div>
<div v-show="showDetails">
<q-card>
<q-card-section class="flex">
<div>
<q-btn
flat
round
icon="arrow_back"
class="q-mr-md"
@click="showDetailsToggle(null)"
></q-btn>
</div>
<div class="self-center text-h6 text-weight-bolder text-grey-5">
<span v-text="$t('payment_details_back')"></span>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section class="text-h6">
<q-item>
<q-item-section avatar class="">
<q-icon color="primary" name="receipt" size="44px"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>
<div class="text-h6">
<span v-text="$t('payment_details')"></span>
</div>
</q-item-label>
<q-item-label caption v-text="$t('payment_details_desc')">
</q-item-label>
</q-item-section>
</q-item>
</q-card-section>
<q-card-section>
<q-list separator>
<q-item v-for="(value, key) in paymentDetails" :key="key">
<q-item-section>
<q-item-label v-text="key"></q-item-label>
<q-item-label
caption
v-text="value"
style="word-wrap: break-word"
></q-item-label>
</q-item-section>
<q-item-section side>
<q-btn
v-show="value"
icon="content_copy"
flat
class="cursor-pointer q-ml-sm"
@click="copyText(value)"
>
<q-tooltip>Copy</q-tooltip>
</q-btn>
</q-item-section>
<!-- <q-separator></q-separator> -->
</q-item>
</q-list>
</q-card-section>
</q-card>
</div>
{% endblock %}
@@ -35,6 +35,7 @@
<q-table :rows="wallets" :columns="walletTable.columns">
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width v-if="g.user.super_user"></q-th>
<q-th auto-width></q-th>
<q-th
auto-width
@@ -47,12 +48,14 @@
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-td auto-width v-if="g.user.super_user">
<lnbits-update-balance
:wallet_id="props.row.id"
@credit-value="handleBalanceUpdate"
class="q-mr-md"
></lnbits-update-balance>
</q-td>
<q-td auto-width>
<q-btn
round
icon="menu"
+45 -12
View File
@@ -1,5 +1,8 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col">
@@ -11,13 +14,35 @@
{%include "users/_createWalletDialog.html" %}
</div>
<div v-else>
<q-btn
@click="showAccountPage()"
:label="$t('create_account')"
color="primary"
class="q-mb-md"
>
</q-btn>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
@click="showAccountPage()"
:label="$t('create_account')"
color="primary"
>
</q-btn>
</div>
<div>
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
to="/admin#users"
>
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
</q-btn>
</div>
</div>
</div>
</q-card>
</div>
</div>
<q-card class="q-pa-md">
<q-table
@@ -102,7 +127,17 @@
:label="props.row.wallet_count"
@click="fetchWallets(props.row.id)"
>
<q-tooltip>Show Wallets</q-tooltip>
</q-btn>
<q-btn
v-if="(users.length == 1) && searchData.wallet_id"
round
icon="menu"
size="sm"
color="secondary"
class="q-ml-sm"
@click="showWalletPayments(searchData.wallet_id)"
>
<q-tooltip>Show Payments</q-tooltip>
</q-btn>
</q-td>
@@ -148,6 +183,4 @@
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/users.js') }}"></script>
{% endblock %}
+128 -9
View File
@@ -1,20 +1,29 @@
import os
import time
from http import HTTPStatus
from shutil import make_archive
from pathlib import Path
from shutil import make_archive, move
from subprocess import Popen
from typing import Optional
from tempfile import NamedTemporaryFile
from typing import IO, Optional
from urllib.parse import urlparse
from fastapi import APIRouter, Depends
import filetype
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
from fastapi.responses import FileResponse
from lnbits.core.models import User
from lnbits.core.models.misc import Image, SimpleStatus
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services import (
enqueue_notification,
get_balance_delta,
update_cached_settings,
)
from lnbits.core.services.notifications import send_email_notification
from lnbits.core.services.settings import dict_to_settings
from lnbits.decorators import check_admin, check_super_user
from lnbits.helpers import safe_upload_file_path
from lnbits.server import server_restart
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
from lnbits.tasks import invoice_listeners
@@ -23,6 +32,7 @@ from .. import core_app_extra
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
file_upload = File(...)
@admin_router.get(
@@ -47,6 +57,18 @@ async def api_monitor():
}
@admin_router.get(
"/api/v1/testemail",
name="TestEmail",
description="send a test email to the admin",
dependencies=[Depends(check_admin)],
)
async def api_test_email():
return await send_email_notification(
"This is a LNbits test email.", "LNbits Test Email"
)
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
async def api_get_settings(
user: User = Depends(check_admin),
@@ -60,6 +82,7 @@ async def api_get_settings(
status_code=HTTPStatus.OK,
)
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await update_admin_settings(data)
admin_settings = await get_admin_settings(user.super_user)
assert admin_settings, "Updated admin settings not found."
@@ -68,6 +91,15 @@ async def api_update_settings(data: UpdateSettings, user: User = Depends(check_a
return {"status": "Success"}
@admin_router.patch(
"/api/v1/settings",
status_code=HTTPStatus.OK,
)
async def api_update_settings_partial(data: dict, user: User = Depends(check_admin)):
updatable_settings = dict_to_settings({**settings.dict(), **data})
return await api_update_settings(updatable_settings, user)
@admin_router.get(
"/api/v1/settings/default",
status_code=HTTPStatus.OK,
@@ -78,12 +110,9 @@ async def api_reset_settings(field_name: str):
return {"default_value": getattr(default_settings, field_name)}
@admin_router.delete(
"/api/v1/settings",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_super_user)],
)
async def api_delete_settings() -> None:
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await delete_admin_settings()
server_restart.set()
@@ -136,3 +165,93 @@ async def api_download_backup() -> FileResponse:
return FileResponse(
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
)
@admin_router.post(
"/api/v1/images",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def upload_image(
file: UploadFile = file_upload,
content_length: int = Header(..., le=settings.lnbits_upload_size_bytes),
) -> Image:
if not file.filename:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="No filename provided."
)
# validate file types
file_info = filetype.guess(file.file)
if file_info is None:
raise HTTPException(
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
detail="Unable to determine file type",
)
detected_content_type = file_info.extension.lower()
if (
file.content_type not in settings.lnbits_upload_allowed_types
or detected_content_type not in settings.lnbits_upload_allowed_types
):
raise HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported file type")
# validate file name
try:
file_path = safe_upload_file_path(file.filename)
except ValueError as e:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail=f"The requested filename '{file.filename}' is forbidden.",
) from e
# validate file size
real_file_size = 0
temp: IO = NamedTemporaryFile(delete=False)
for chunk in file.file:
real_file_size += len(chunk)
if real_file_size > content_length:
raise HTTPException(
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
detail=f"File too large ({content_length / 1000} KB max)",
)
temp.write(chunk)
temp.close()
move(temp.name, file_path)
return Image(filename=file.filename)
@admin_router.get(
"/api/v1/images",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def list_uploaded_images() -> list[Image]:
image_folder = Path(settings.lnbits_data_folder, "images")
files = image_folder.glob("*")
images = []
for file in files:
if file.is_file():
images.append(Image(filename=file.name))
return images
@admin_router.delete(
"/api/v1/images/{filename}",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def delete_uploaded_image(filename: str) -> SimpleStatus:
try:
file_path = safe_upload_file_path(filename)
except ValueError as e:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail=f"The requested filename '{filename}' is forbidden.",
) from e
if not file_path.exists():
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Image not found.")
file_path.unlink()
return SimpleStatus(success=True, message=f"{filename} deleted")
+7 -9
View File
@@ -15,7 +15,6 @@ from fastapi import (
from fastapi.exceptions import HTTPException
from fastapi.responses import StreamingResponse
from lnbits.core.crud import get_user
from lnbits.core.models import (
BaseWallet,
ConversionData,
@@ -30,6 +29,7 @@ from lnbits.decorators import (
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import check_callback_url
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import (
@@ -50,21 +50,18 @@ api_router = APIRouter(tags=["Core"])
async def health() -> dict:
return {
"server_time": int(time()),
"up_time": int(time() - settings.server_startup_time),
"up_time": settings.lnbits_server_up_time,
}
@api_router.get("/api/v1/status", status_code=HTTPStatus.OK)
async def health_check(wallet: WalletTypeInfo = Depends(require_invoice_key)) -> dict:
async def health_check(user: User = Depends(check_user_exists)) -> dict:
stat: dict[str, Any] = {
"server_time": int(time()),
"up_time": int(time() - settings.server_startup_time),
"up_time": settings.lnbits_server_up_time,
"up_time_seconds": int(time() - settings.server_startup_time),
}
user = await get_user(wallet.wallet.user)
if not user:
return stat
stat["version"] = settings.version
if not user.admin:
return stat
@@ -132,6 +129,7 @@ async def api_lnurlscan(
else:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
check_callback_url(url)
r = await client.get(url, timeout=5)
r.raise_for_status()
if r.is_error:
@@ -227,7 +225,7 @@ async def api_perform_lnurlauth(
@api_router.get(
"/api/v1/rate/history",
dependencies=[Depends(require_invoice_key)],
dependencies=[Depends(check_user_exists)],
)
async def api_exchange_rate_history() -> list[dict]:
return settings.lnbits_exchange_rate_history
+148
View File
@@ -11,12 +11,26 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase
from loguru import logger
from lnbits.core.crud.users import (
get_user_access_control_lists,
update_user_access_control_list,
)
from lnbits.core.models.misc import SimpleItem
from lnbits.core.models.users import (
ApiTokenRequest,
ApiTokenResponse,
DeleteAccessControlList,
DeleteTokenRequest,
EndpointAccess,
UpdateAccessControlList,
)
from lnbits.core.services import create_user_account
from lnbits.decorators import access_token_payload, check_user_exists
from lnbits.helpers import (
create_access_token,
decrypt_internal_message,
encrypt_internal_message,
get_api_routes,
is_valid_email_address,
is_valid_username,
)
@@ -44,6 +58,7 @@ from ..models import (
UpdateUserPassword,
UpdateUserPubkey,
User,
UserAcls,
UserExtra,
)
@@ -95,9 +110,131 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
account = await get_account(data.usr)
if not account:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
if account.is_admin:
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "Admin users cannot login with user id only."
)
return _auth_success_response(account.username, account.id, account.email)
@auth_router.get("/acl")
async def api_get_user_acls(
request: Request,
user: User = Depends(check_user_exists),
) -> UserAcls:
api_routes = get_api_routes(request.app.router.routes)
acls = await get_user_access_control_lists(user.id)
# Add missing/new endpoints to the ACLs
for acl in acls.access_control_list:
acl_api_routes = {**api_routes}
for route in api_routes.keys():
if acl.get_endpoint(route):
acl_api_routes.pop(route, None)
for path, name in acl_api_routes.items():
acl.endpoints.append(EndpointAccess(path=path, name=name))
acl.endpoints.sort(key=lambda e: e.name.lower())
return UserAcls(id=user.id, access_control_list=acls.access_control_list)
@auth_router.put("/acl")
@auth_router.patch("/acl")
async def api_update_user_acl(
request: Request,
data: UpdateAccessControlList,
user: User = Depends(check_user_exists),
) -> UserAcls:
account = await get_account(user.id)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
user_acls = await get_user_access_control_lists(user.id)
acl = user_acls.get_acl_by_id(data.id)
if acl:
user_acls.access_control_list.remove(acl)
else:
data.endpoints = []
data.id = uuid4().hex
api_routes = get_api_routes(request.app.router.routes)
for path, name in api_routes.items():
data.endpoints.append(EndpointAccess(path=path, name=name))
api_paths = get_api_routes(request.app.router.routes).keys()
data.endpoints = [e for e in data.endpoints if e.path in api_paths]
data.endpoints.sort(key=lambda e: e.name.lower())
user_acls.access_control_list.append(data)
user_acls.access_control_list.sort(key=lambda t: t.name.lower())
await update_user_access_control_list(user_acls)
return user_acls
@auth_router.delete("/acl")
async def api_delete_user_acl(
data: DeleteAccessControlList,
user: User = Depends(check_user_exists),
):
account = await get_account(user.id)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
user_acls = await get_user_access_control_lists(user.id)
user_acls.delete_acl_by_id(data.id)
await update_user_access_control_list(user_acls)
@auth_router.post("/acl/token")
async def api_create_user_api_token(
data: ApiTokenRequest,
user: User = Depends(check_user_exists),
) -> ApiTokenResponse:
assert data.expiration_time_minutes > 0, "Expiration time must be in the future."
account = await get_account(user.id)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
assert account.username, "Username must be configured."
acls = await get_user_access_control_lists(user.id)
acl = acls.get_acl_by_id(data.acl_id)
if not acl:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
api_token_id = uuid4().hex
api_token = _auth_api_token_response(
account.username, api_token_id, data.expiration_time_minutes
)
acl.token_id_list.append(SimpleItem(id=api_token_id, name=data.token_name))
await update_user_access_control_list(acls)
return ApiTokenResponse(id=api_token_id, api_token=api_token)
@auth_router.delete("/acl/token")
async def api_delete_user_api_token(
data: DeleteTokenRequest,
user: User = Depends(check_user_exists),
):
account = await get_account(user.id)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
assert account.username, "Username must be configured."
acls = await get_user_access_control_lists(user.id)
acl = acls.get_acl_by_id(data.acl_id)
if not acl:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid ACL id.")
acl.delete_token_by_id(data.id)
await update_user_access_control_list(acls)
@auth_router.get("/{provider}", description="SSO Provider")
async def login_with_sso_provider(
request: Request, provider: str, user_id: Optional[str] = None
@@ -370,6 +507,17 @@ def _auth_success_response(
return response
def _auth_api_token_response(
username: str, api_token_id: str, token_expire_minutes: int
):
payload = AccessTokenPayload(
sub=username, api_token_id=api_token_id, auth_time=int(time())
)
return create_access_token(
data=payload.dict(), token_expire_minutes=token_expire_minutes
)
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
access_token = create_access_token(data=payload.dict())
+51 -34
View File
@@ -1,5 +1,4 @@
from http import HTTPStatus
from pathlib import Path
from typing import Annotated, List, Optional, Union
from urllib.parse import urlencode, urlparse
@@ -17,7 +16,7 @@ from lnbits.core.models.extensions import ExtensionMeta, InstallableExtension
from lnbits.core.services import create_invoice, create_user_account
from lnbits.core.services.extensions import get_valid_extensions
from lnbits.decorators import check_admin, check_user_exists
from lnbits.helpers import template_renderer
from lnbits.helpers import check_callback_url, template_renderer
from lnbits.settings import settings
from lnbits.wallets import get_funding_source
@@ -37,7 +36,7 @@ generic_router = APIRouter(
@generic_router.get("/favicon.ico", response_class=FileResponse)
async def favicon():
return FileResponse(Path("lnbits", "static", "favicon.ico"))
return RedirectResponse(settings.lnbits_qr_logo)
@generic_router.get("/", response_class=HTMLResponse)
@@ -50,12 +49,9 @@ 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(
request,
"error.html",
{
"err": "Super user account has already been configured.",
},
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Super user account has already been configured.",
)
return template_renderer().TemplateResponse(
request,
@@ -107,7 +103,7 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
db_versions = await get_db_versions()
extensions = [
extension_data = [
{
"id": ext.id,
"name": ext.name,
@@ -152,7 +148,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"core/extensions.html",
{
"user": user.json(),
"extensions": extensions,
"extension_data": extension_data,
"ajax": _is_ajax_request(request),
},
)
@@ -179,25 +176,25 @@ async def wallet(
wallet = user.wallets[0]
if not wallet or wallet.deleted:
return template_renderer().TemplateResponse(
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet not found",
)
context = {
"user": user.json(),
"wallet": wallet.json(),
"wallet_name": wallet.name,
"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 = template_renderer().TemplateResponse(
return template_renderer().TemplateResponse(
request,
"core/wallet.html",
{
"user": user.json(),
"wallet": wallet.json(),
"wallet_name": wallet.name,
"currencies": allowed_currencies(),
"service_fee": settings.lnbits_service_fee,
"service_fee_max": settings.lnbits_service_fee_max,
"web_manifest": f"/manifest/{user.id}.webmanifest",
},
{**context, "ajax": _is_ajax_request(request)},
)
resp.set_cookie("lnbits_last_active_wallet", wallet.id)
return resp
@generic_router.get(
@@ -214,6 +211,7 @@ async def account(
"core/account.html",
{
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@@ -327,6 +325,7 @@ async def node(request: Request, user: User = Depends(check_admin)):
"settings": settings.dict(),
"balance": balance,
"wallets": user.wallets[0].json(),
"ajax": _is_ajax_request(request),
},
)
@@ -365,6 +364,7 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
"settings": settings.dict(),
"balance": balance,
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
)
@@ -381,6 +381,7 @@ async def users_index(request: Request, user: User = Depends(check_admin)):
"user": user.json(),
"settings": settings.dict(),
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
)
@@ -395,6 +396,19 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
{
"request": request,
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@generic_router.get("/payments", response_class=HTMLResponse)
async def payments_index(request: Request, user: User = Depends(check_admin)):
return template_renderer().TemplateResponse(
"payments/index.html",
{
"request": request,
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@@ -411,23 +425,22 @@ async def hex_to_uuid4(hex_value: str):
@generic_router.get("/lnurlwallet", response_class=RedirectResponse)
async def lnurlwallet(request: Request):
async def lnurlwallet(request: Request, lightning: str = ""):
"""
If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with
their smartphone camera, or a QR scanner app, they can follow the link provided to
claim their satoshis and get an instant LNbits wallet! lnbits/withdraw docs
"""
lightning_param = request.query_params.get("lightning")
if not lightning_param:
if not lightning:
return {"status": "ERROR", "reason": "lightning parameter not provided."}
if not settings.lnbits_allow_new_accounts:
return {"status": "ERROR", "reason": "New accounts are not allowed."}
lnurl = lnurl_decode(lightning_param)
lnurl = lnurl_decode(lightning)
async with httpx.AsyncClient() as client:
check_callback_url(lnurl)
res1 = await client.get(lnurl, timeout=2)
res1.raise_for_status()
data1 = res1.json()
@@ -442,19 +455,23 @@ async def lnurlwallet(request: Request):
detail="Invalid lnurl. Expected maxWithdrawable",
)
account = await create_user_account()
wallet = await create_wallet(user_id=account.id)
_, payment_request = await create_invoice(
wallet = account.wallets[0]
payment = await create_invoice(
wallet_id=wallet.id,
amount=data1.get("maxWithdrawable") / 1000,
memo=data1.get("defaultDescription", "lnurl wallet withdraw"),
)
url = data1.get("callback")
params = {"k1": data1.get("k1"), "pr": payment_request}
params = {"k1": data1.get("k1"), "pr": payment.bolt11}
callback = url + ("&" if urlparse(url).query else "?") + urlencode(params)
res2 = await client.get(callback, timeout=2)
res2 = await client.get(callback, timeout=5)
res2.raise_for_status()
return RedirectResponse(
f"/wallet?usr={account.id}&wal={wallet.id}",
)
def _is_ajax_request(request: Request):
return request.headers.get("X-Requested-With", None) == "XMLHttpRequest"
+21 -3
View File
@@ -95,6 +95,14 @@ async def api_get_channels(
return await node.get_channels()
@node_router.get("/channels/{channel_id}")
async def api_get_channel(
channel_id: str,
node: Node = Depends(require_node),
) -> Optional[NodeChannel]:
return await node.get_channel(channel_id)
@super_node_router.post("/channels", response_model=ChannelPoint)
async def api_create_channel(
node: Node = Depends(require_node),
@@ -125,7 +133,17 @@ async def api_delete_channel(
)
@node_router.get("/payments", response_model=Page[NodePayment])
@super_node_router.put("/channels/{channel_id}")
async def api_set_channel_fees(
channel_id: str,
node: Node = Depends(require_node),
fee_ppm: int = Body(None),
fee_base_msat: int = Body(None),
):
await node.set_channel_fee(channel_id, fee_base_msat, fee_ppm)
@node_router.get("/payments")
async def api_get_payments(
node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
@@ -138,7 +156,7 @@ async def api_get_payments(
return await node.get_payments(filters)
@node_router.get("/invoices", response_model=Page[NodeInvoice])
@node_router.get("/invoices")
async def api_get_invoices(
node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
@@ -151,7 +169,7 @@ async def api_get_invoices(
return await node.get_invoices(filters)
@node_router.get("/peers", response_model=List[NodePeerInfo])
@node_router.get("/peers")
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
return await node.get_peers()
+105 -4
View File
@@ -1,4 +1,5 @@
import json
import ssl
from http import HTTPStatus
from math import ceil
from typing import List, Optional
@@ -16,6 +17,10 @@ from fastapi.responses import JSONResponse
from loguru import logger
from lnbits import bolt11
from lnbits.core.crud.payments import (
get_payment_count_stats,
get_wallets_stats,
)
from lnbits.core.models import (
CreateInvoice,
CreateLnurl,
@@ -23,18 +28,33 @@ from lnbits.core.models import (
KeyType,
PayLnurlWData,
Payment,
PaymentCountField,
PaymentCountStat,
PaymentDailyStats,
PaymentFilters,
PaymentHistoryPoint,
PaymentWalletStats,
Wallet,
)
from lnbits.core.models.users import User
from lnbits.core.services.payments import (
get_payments_daily_stats,
update_pending_payment,
)
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
check_user_exists,
parse_filters,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import filter_dict_keys, generate_filter_params_openapi
from lnbits.helpers import (
check_callback_url,
filter_dict_keys,
generate_filter_params_openapi,
)
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
@@ -93,6 +113,64 @@ async def api_payments_history(
return await get_payments_history(key_info.wallet.id, group, filters)
@payment_router.get(
"/stats/count",
name="Get payments history for all users",
dependencies=[Depends(check_admin)],
response_model=List[PaymentCountStat],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_counting_stats(
count_by: PaymentCountField = Query("tag"),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
return await get_payment_count_stats(count_by, filters)
@payment_router.get(
"/stats/wallets",
name="Get payments history for all users",
dependencies=[Depends(check_admin)],
response_model=List[PaymentWalletStats],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_wallets_stats(
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
return await get_wallets_stats(filters)
@payment_router.get(
"/stats/daily",
name="Get payments history per day",
response_model=List[PaymentDailyStats],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_daily_stats(
user: User = Depends(check_user_exists),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
if not user.admin:
exc = HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Missing wallet id.",
)
wallet_filter = next(
(f for f in filters.filters if f.field == "wallet_id"), None
)
if not wallet_filter:
raise exc
wallet_id = list((wallet_filter.values or {}).values())
if len(wallet_id) == 0:
raise exc
if not user.get_wallet(wallet_id[0]):
raise exc
return await get_payments_daily_stats(filters)
@payment_router.get(
"/paginated",
name="Payment List",
@@ -105,11 +183,14 @@ async def api_payments_paginated(
key_info: WalletTypeInfo = Depends(require_invoice_key),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(key_info.wallet.id)
page = await get_payments_paginated(
wallet_id=key_info.wallet.id,
filters=filters,
)
for payment in page.data:
if payment.pending:
await update_pending_payment(payment)
return page
@@ -155,6 +236,7 @@ async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(data.lnurl_callback)
r = await client.get(
data.lnurl_callback,
params={"pr": payment.bolt11},
@@ -175,6 +257,23 @@ async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
return payment
@payment_router.get(
"/all/paginated",
name="Payment List",
summary="get paginated list of payments",
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
dependencies=[Depends(check_admin)],
)
async def api_all_payments_paginated(
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
return await get_payments_paginated(
filters=filters,
)
@payment_router.post(
"",
summary="Create or pay an invoice",
@@ -250,6 +349,7 @@ async def api_payments_pay_lnurl(
amount_msat = ceil(amount_msat // 1000) * 1000
else:
amount_msat = data.amount
check_callback_url(data.callback)
r = await client.get(
data.callback,
params={"amount": amount_msat, "comment": data.comment},
@@ -258,7 +358,8 @@ async def api_payments_pay_lnurl(
if r.is_error:
raise httpx.ConnectError("LNURL callback connection error")
r.raise_for_status()
except (httpx.ConnectError, httpx.RequestError) as exc:
except (httpx.HTTPError, ssl.SSLError) as exc:
logger.warning(exc)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}.",
@@ -366,7 +467,6 @@ async def api_payment_pay_with_nfc(
payment_request: str,
lnurl_data: PayLnurlWData,
) -> JSONResponse:
lnurl = lnurl_data.lnurl_w.lower()
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
@@ -375,6 +475,7 @@ async def api_payment_pay_with_nfc(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
try:
check_callback_url(url)
lnurl_req = await client.get(url, timeout=10)
if lnurl_req.is_error:
return JSONResponse(
+32 -3
View File
@@ -31,9 +31,11 @@ from lnbits.core.models import (
UserExtra,
Wallet,
)
from lnbits.core.models.notifications import NotificationType
from lnbits.core.models.users import Account
from lnbits.core.services import (
create_user_account_no_ckeck,
enqueue_notification,
update_user_account,
update_user_extensions,
update_wallet_balance,
@@ -102,16 +104,24 @@ async def api_create_user(data: CreateUser) -> CreateUser:
)
account.validate_fields()
account.hash_password(data.password)
user = await create_user_account_no_ckeck(account)
user = await create_user_account_no_ckeck(account, default_exts=data.extensions)
data.id = user.id
return data
@users_router.put("/user/{user_id}", name="Update user")
async def api_update_user(user_id: str, data: CreateUser) -> CreateUser:
async def api_update_user(
user_id: str, data: CreateUser, user: User = Depends(check_admin)
) -> CreateUser:
if user_id != data.id:
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
if user_id == settings.super_user and user.id != settings.super_user:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Action only allowed for super user.",
)
if data.password or data.password_repeat:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
@@ -253,13 +263,22 @@ async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleSta
summary="First time it is called it does a soft delete (only sets a flag)."
"The second time it is called will delete the entry from the DB",
)
async def api_users_delete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
async def api_users_delete_user_wallet(
user_id: str, wallet: str, user: User = Depends(check_admin)
) -> SimpleStatus:
wal = await get_wallet(wallet)
if not wal:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet does not exist.",
)
if user_id == settings.super_user and user.id != settings.super_user:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Action only allowed for super user.",
)
if wal.deleted:
await force_delete_wallet(wallet)
await delete_wallet(user_id=user_id, wallet_id=wallet)
@@ -277,4 +296,14 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
if not wallet:
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
enqueue_notification(
NotificationType.balance_update,
{
"amount": int(data.amount),
"wallet_id": wallet.id,
"wallet_name": wallet.name,
"balance": wallet.balance,
},
)
return SimpleStatus(success=True, message="Balance updated.")
+29 -9
View File
@@ -1,5 +1,6 @@
from http import HTTPStatus
from typing import Optional
from uuid import uuid4
from fastapi import (
APIRouter,
@@ -8,13 +9,10 @@ from fastapi import (
HTTPException,
)
from lnbits.core.models import (
CreateWallet,
KeyType,
Wallet,
)
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
from lnbits.decorators import (
WalletTypeInfo,
check_user_exists,
require_admin_key,
require_invoice_key,
)
@@ -56,9 +54,25 @@ async def api_update_wallet_name(
}
@wallet_router.put("/reset/{wallet_id}")
async def api_reset_wallet_keys(
wallet_id: str, user: User = Depends(check_user_exists)
) -> Wallet:
wallet = await get_wallet(wallet_id)
if not wallet or wallet.user != user.id:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
wallet.adminkey = uuid4().hex
wallet.inkey = uuid4().hex
await update_wallet(wallet)
return wallet
@wallet_router.patch("")
async def api_update_wallet(
name: Optional[str] = Body(None),
icon: Optional[str] = Body(None),
color: Optional[str] = Body(None),
currency: Optional[str] = Body(None),
key_info: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet:
@@ -66,18 +80,24 @@ async def api_update_wallet(
if not wallet:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
wallet.name = name or wallet.name
wallet.extra.icon = icon or wallet.extra.icon
wallet.extra.color = color or wallet.extra.color
wallet.currency = currency if currency is not None else wallet.currency
await update_wallet(wallet)
return wallet
@wallet_router.delete("")
@wallet_router.delete("/{wallet_id}")
async def api_delete_wallet(
wallet: WalletTypeInfo = Depends(require_admin_key),
wallet_id: str, user: User = Depends(check_user_exists)
) -> None:
wallet = await get_wallet(wallet_id)
if not wallet or wallet.user != user.id:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
await delete_wallet(
user_id=wallet.wallet.user,
wallet_id=wallet.wallet.id,
user_id=wallet.user,
wallet_id=wallet.id,
)
+7 -7
View File
@@ -15,9 +15,9 @@ from lnbits.core.models import (
CreateWebPushSubscription,
WebPushSubscription,
)
from lnbits.core.models.users import User
from lnbits.decorators import (
WalletTypeInfo,
require_admin_key,
check_user_exists,
)
from ..crud import (
@@ -33,20 +33,20 @@ webpush_router = APIRouter(prefix="/api/v1/webpush", tags=["Webpush"])
async def api_create_webpush_subscription(
request: Request,
data: CreateWebPushSubscription,
wallet: WalletTypeInfo = Depends(require_admin_key),
user: User = Depends(check_user_exists),
) -> WebPushSubscription:
try:
subscription = json.loads(data.subscription)
endpoint = subscription["endpoint"]
host = urlparse(str(request.url)).netloc
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
subscription = await get_webpush_subscription(endpoint, user.id)
if subscription:
return subscription
else:
return await create_webpush_subscription(
endpoint,
wallet.wallet.user,
user.id,
data.subscription,
host,
)
@@ -61,13 +61,13 @@ async def api_create_webpush_subscription(
@webpush_router.delete("", status_code=HTTPStatus.OK)
async def api_delete_webpush_subscription(
request: Request,
wallet: WalletTypeInfo = Depends(require_admin_key),
user: User = Depends(check_user_exists),
):
try:
endpoint = unquote(
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
)
count = await delete_webpush_subscription(endpoint, wallet.wallet.user)
count = await delete_webpush_subscription(endpoint, user.id)
return {"count": count}
except Exception as exc:
logger.debug(exc)
+59 -41
View File
@@ -8,7 +8,7 @@ import time
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Generic, Literal, Optional, TypeVar, Union, get_origin
from typing import Any, Generic, Literal, TypeVar, get_origin
from loguru import logger
from pydantic import BaseModel, ValidationError, root_validator
@@ -22,6 +22,13 @@ POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
SQLITE = "SQLITE"
DateTrunc = Literal["hour", "day", "month"]
sqlite_formats = {
"hour": "%Y-%m-%d %H:00:00",
"day": "%Y-%m-%d 00:00:00",
"month": "%Y-%m-01 00:00:00",
}
if settings.lnbits_database_url:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
@@ -58,8 +65,8 @@ def get_placeholder(model: Any, field: str) -> str:
class Compat:
type: Optional[str] = "<inherited>"
schema: Optional[str] = "<inherited>"
type: str | None = "<inherited>"
schema: str | None = "<inherited>"
def interval_seconds(self, seconds: int) -> str:
if self.type in {POSTGRES, COCKROACH}:
@@ -75,6 +82,18 @@ class Compat:
return time.mktime(date.timetuple())
return "<nothing>"
def datetime_grouping(self, group: DateTrunc):
if self.type in {POSTGRES, COCKROACH}:
return f"date_trunc('{group}', time)"
elif self.type == SQLITE:
return (
"CAST (strftime('%s',datetime(strftime("
f"'{sqlite_formats[group]}'"
", time, 'unixepoch'))) AS INT)"
)
return "<bad grouping>"
@property
def timestamp_now(self) -> str:
if self.type in {POSTGRES, COCKROACH}:
@@ -148,8 +167,8 @@ class Connection(Compat):
async def fetchall(
self,
query: str,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
values: dict | None = None,
model: type[TModel] | None = None,
) -> list[TModel]:
params = self.rewrite_values(values) if values else {}
result = await self.conn.execute(text(self.rewrite_query(query)), params)
@@ -164,8 +183,8 @@ class Connection(Compat):
async def fetchone(
self,
query: str,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
values: dict | None = None,
model: type[TModel] | None = None,
) -> TModel:
params = self.rewrite_values(values) if values else {}
result = await self.conn.execute(text(self.rewrite_query(query)), params)
@@ -192,11 +211,11 @@ class Connection(Compat):
async def fetch_page(
self,
query: str,
where: Optional[list[str]] = None,
values: Optional[dict] = None,
filters: Optional[Filters] = None,
model: Optional[type[TModel]] = None,
group_by: Optional[list[str]] = None,
where: list[str] | None = None,
values: dict | None = None,
filters: Filters | None = None,
model: type[TModel] | None = None,
group_by: list[str] | None = None,
) -> Page[TModel]:
if not filters:
filters = Filters()
@@ -249,7 +268,7 @@ class Connection(Compat):
total=count,
)
async def execute(self, query: str, values: Optional[dict] = None):
async def execute(self, query: str, values: dict | None = None):
params = self.rewrite_values(values) if values else {}
result = await self.conn.execute(text(self.rewrite_query(query)), params)
await self.conn.commit()
@@ -331,8 +350,8 @@ class Database(Compat):
async def fetchall(
self,
query: str,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
values: dict | None = None,
model: type[TModel] | None = None,
) -> list[TModel]:
async with self.connect() as conn:
return await conn.fetchall(query, values, model)
@@ -340,8 +359,8 @@ class Database(Compat):
async def fetchone(
self,
query: str,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
values: dict | None = None,
model: type[TModel] | None = None,
) -> TModel:
async with self.connect() as conn:
return await conn.fetchone(query, values, model)
@@ -359,16 +378,16 @@ class Database(Compat):
async def fetch_page(
self,
query: str,
where: Optional[list[str]] = None,
values: Optional[dict] = None,
filters: Optional[Filters] = None,
model: Optional[type[TModel]] = None,
group_by: Optional[list[str]] = None,
where: list[str] | None = None,
values: dict | None = None,
filters: Filters | None = None,
model: type[TModel] | None = None,
group_by: list[str] | None = None,
) -> Page[TModel]:
async with self.connect() as conn:
return await conn.fetch_page(query, where, values, filters, model, group_by)
async def execute(self, query: str, values: Optional[dict] = None):
async def execute(self, query: str, values: dict | None = None):
async with self.connect() as conn:
return await conn.execute(query, values)
@@ -426,7 +445,7 @@ class Operator(Enum):
class FilterModel(BaseModel):
__search_fields__: list[str] = []
__sort_fields__: Optional[list[str]] = None
__sort_fields__: list[str] | None = None
T = TypeVar("T")
@@ -442,8 +461,8 @@ class Page(BaseModel, Generic[T]):
class Filter(BaseModel, Generic[TFilterModel]):
field: str
op: Operator = Operator.EQ
model: Optional[type[TFilterModel]]
values: Optional[dict] = None
model: type[TFilterModel] | None
values: dict | None = None
@classmethod
def parse_query(
@@ -498,15 +517,15 @@ class Filters(BaseModel, Generic[TFilterModel]):
"""
filters: list[Filter[TFilterModel]] = []
search: Optional[str] = None
search: str | None = None
offset: Optional[int] = None
limit: Optional[int] = None
offset: int | None = None
limit: int | None = None
sortby: Optional[str] = None
direction: Optional[Literal["asc", "desc"]] = None
sortby: str | None = None
direction: Literal["asc", "desc"] | None = None
model: Optional[type[TFilterModel]] = None
model: type[TFilterModel] | None = None
@root_validator(pre=True)
def validate_sortby(cls, values):
@@ -528,18 +547,17 @@ class Filters(BaseModel, Generic[TFilterModel]):
stmt += f"OFFSET {self.offset}"
return stmt
def where(self, where_stmts: Optional[list[str]] = None) -> str:
def where(self, where_stmts: list[str] | None = None) -> str:
if not where_stmts:
where_stmts = []
if self.filters:
for page_filter in self.filters:
where_stmts.append(page_filter.statement)
if self.search and self.model:
fields = self.model.__search_fields__
if DB_TYPE == POSTGRES:
where_stmts.append(f"lower(concat({', '.join(fields)})) LIKE :search")
elif DB_TYPE == SQLITE:
where_stmts.append(f"lower({'||'.join(fields)}) LIKE :search")
if self.search and self.model and self.model.__search_fields__:
where_stmts.append(
f"lower(concat({', '.join(self.model.__search_fields__)})) LIKE :search"
)
if where_stmts:
return "WHERE " + " AND ".join(where_stmts)
return ""
@@ -549,7 +567,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
return ""
def values(self, values: Optional[dict] = None) -> dict:
def values(self, values: dict | None = None) -> dict:
if not values:
values = {}
if self.filters:
@@ -623,7 +641,7 @@ def model_to_dict(model: BaseModel) -> dict:
return _dict
def dict_to_submodel(model: type[TModel], value: Union[dict, str]) -> Optional[TModel]:
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
"""convert a dictionary or JSON string to a Pydantic model"""
if isinstance(value, str):
if value == "null":
+77 -27
View File
@@ -5,7 +5,7 @@ import jwt
from fastapi import Cookie, Depends, Query, Request, Security
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
from fastapi.security import APIKeyHeader, APIKeyQuery, HTTPBearer, OAuth2PasswordBearer
from fastapi.security.base import SecurityBase
from loguru import logger
from pydantic.types import UUID4
@@ -18,6 +18,7 @@ from lnbits.core.crud import (
get_user_from_account,
get_wallet_for_key,
)
from lnbits.core.crud.users import get_user_access_control_lists
from lnbits.core.models import (
AccessTokenPayload,
Account,
@@ -27,10 +28,18 @@ from lnbits.core.models import (
WalletTypeInfo,
)
from lnbits.db import Connection, Filter, Filters, TFilterModel
from lnbits.helpers import path_segments
from lnbits.settings import AuthMethods, settings
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth", auto_error=False)
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="api/v1/auth",
auto_error=False,
description="OAuth2 access token for authentication with username and password.",
)
http_bearer = HTTPBearer(
auto_error=False,
description="Bearer Token for custom ACL based access control",
)
api_key_header = APIKeyHeader(
name="X-API-KEY",
auto_error=False,
@@ -130,8 +139,9 @@ async def require_invoice_key(
async def check_access_token(
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None,
) -> Optional[str]:
return header_access_token or cookie_access_token
return header_access_token or cookie_access_token or bearer_access_token
async def check_user_exists(
@@ -140,9 +150,13 @@ async def check_user_exists(
usr: Optional[UUID4] = None,
) -> User:
if access_token:
account = await _get_account_from_token(access_token)
account = await _get_account_from_token(access_token, r["path"], r["method"])
elif usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
account = await get_account(usr.hex)
if account and account.is_admin:
raise HTTPException(
HTTPStatus.FORBIDDEN, "User id only access for admins is forbidden."
)
else:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing user ID or access token.")
@@ -161,13 +175,14 @@ async def check_user_exists(
async def optional_user_id(
r: Request,
access_token: Annotated[Optional[str], Depends(check_access_token)],
usr: Optional[UUID4] = None,
) -> Optional[str]:
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
return usr.hex
if access_token:
account = await _get_account_from_token(access_token)
account = await _get_account_from_token(access_token, r["path"], r["method"])
return account.id if account else None
return None
@@ -188,6 +203,10 @@ async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
)
if not user.has_password:
raise HTTPException(
HTTPStatus.FORBIDDEN, "Admin users must have credentials configured."
)
return user
@@ -197,6 +216,10 @@ async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) ->
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
)
if not user.has_password:
raise HTTPException(
HTTPStatus.FORBIDDEN, "Super user must have credentials configured."
)
return user
@@ -247,19 +270,18 @@ async def check_user_extension_access(
success=False, message=f"User not authorized for extension '{ext_id}'."
)
if settings.is_extension_id(ext_id):
if settings.is_installed_extension_id(ext_id):
ext_ids = await get_user_active_extensions_ids(user_id, conn=conn)
if ext_id not in ext_ids:
return SimpleStatus(
success=False, message=f"User extension '{ext_id}' not enabled."
success=False, message=f"Extension '{ext_id}' not enabled."
)
return SimpleStatus(success=True, message="OK")
async def _check_user_extension_access(user_id: str, current_path: str):
path = current_path.split("/")
ext_id = path[3] if path[1] == "upgrades" else path[1]
async def _check_user_extension_access(user_id: str, path: str):
ext_id = path_segments(path)[0]
status = await check_user_extension_access(user_id, ext_id)
if not status.success:
raise HTTPException(
@@ -268,16 +290,14 @@ async def _check_user_extension_access(user_id: str, current_path: str):
)
async def _get_account_from_token(access_token) -> Optional[Account]:
async def _get_account_from_token(
access_token: str, path: str, method: str
) -> Optional[Account]:
try:
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
user = await _get_user_from_jwt_payload(payload)
if not user:
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "Data missing for access token."
)
return user
return await _get_account_from_jwt_payload(
AccessTokenPayload(**payload), path, method
)
except jwt.ExpiredSignatureError as exc:
raise HTTPException(
@@ -288,11 +308,41 @@ async def _get_account_from_token(access_token) -> Optional[Account]:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid access token.") from exc
async def _get_user_from_jwt_payload(payload) -> Optional[Account]:
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")))
return None
async def _get_account_from_jwt_payload(
payload: AccessTokenPayload, path: str, method: str
) -> Optional[Account]:
account = None
if payload.sub:
account = await get_account_by_username(payload.sub)
elif payload.usr:
account = await get_account(payload.usr)
elif payload.email:
account = await get_account_by_email(payload.email)
if not account:
return None
if payload.api_token_id:
await _check_account_api_access(account.id, payload.api_token_id, path, method)
return account
async def _check_account_api_access(
user_id: str, token_id: str, path: str, method: str
):
segments = path.split("/")
if len(segments) < 3:
raise HTTPException(HTTPStatus.FORBIDDEN, "Not an API endpoint.")
acls = await get_user_access_control_lists(user_id)
acl = acls.get_acl_by_token_id(token_id)
if not acl:
raise HTTPException(HTTPStatus.FORBIDDEN, "Invalid token id.")
path = "/" + "/".join(path_segments(path)[:3])
endpoint = acl.get_endpoint(path)
if not endpoint:
raise HTTPException(HTTPStatus.FORBIDDEN, "Path not allowed.")
if not endpoint.supports_method(method):
raise HTTPException(HTTPStatus.FORBIDDEN, "Method not allowed.")
+54 -3
View File
@@ -8,7 +8,9 @@ from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, RedirectResponse, Response
from loguru import logger
from .helpers import template_renderer
from lnbits.settings import settings
from .helpers import path_segments, template_renderer
class PaymentError(Exception):
@@ -26,9 +28,11 @@ class InvoiceError(Exception):
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if not request.headers:
return None
if "text/html" not in request.headers.get("accept", ""):
if not _is_browser_request(request):
return None
if (
@@ -49,7 +53,14 @@ def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
)
return template_renderer().TemplateResponse(
request, "error.html", {"err": f"Error: {exc!s}"}, status_code
request,
"error.html",
{
"err": f"Error: {exc!s}",
"status_code": int(status_code),
"message": str(exc).split(":")[-1].strip(),
},
status_code,
)
@@ -119,3 +130,43 @@ def register_exception_handlers(app: FastAPI):
status_code=520,
content={"detail": exc.message, "status": exc.status},
)
@app.exception_handler(404)
async def error_handler_404(request: Request, exc: HTTPException):
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
if not _is_browser_request(request):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
path = path_segments(request.url.path)[0]
status_code = HTTPStatus.NOT_FOUND
message: str = "Page not found."
if path in settings.lnbits_all_extensions_ids:
status_code = HTTPStatus.FORBIDDEN
message = f"Extension '{path}' not installed. Ask the admin to install it."
return template_renderer().TemplateResponse(
request,
"error.html",
{"status_code": int(status_code), "message": message},
int(status_code),
)
def _is_browser_request(request: Request) -> bool:
# Check a few common browser agents, also not fail proof
if "api/v1" in request.url.path:
return False
browser_agents = ["Mozilla", "Chrome", "Safari"]
if any(agent in request.headers.get("user-agent", "") for agent in browser_agents):
return True
if "text/html" in request.headers.get("accept", ""):
return True
return False
+102 -14
View File
@@ -5,10 +5,12 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional, Type
from urllib import request
from urllib.parse import urlparse
import jinja2
import jwt
import shortuuid
from fastapi.routing import APIRoute
from packaging import version
from pydantic.schema import field_schema
@@ -69,12 +71,17 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
t.env.globals["LNBITS_DENOMINATION"] = (
settings.lnbits_denomination
if settings.lnbits_backend_wallet_class == "FakeWallet"
else "sats"
)
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
settings.lnbits_show_home_page_elements
)
t.env.globals["LNBITS_CUSTOM_IMAGE"] = settings.lnbits_custom_image
t.env.globals["LNBITS_CUSTOM_BADGE"] = settings.lnbits_custom_badge
t.env.globals["LNBITS_CUSTOM_BADGE_COLOR"] = settings.lnbits_custom_badge_color
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options
@@ -95,11 +102,26 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
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"] = list(settings.lnbits_all_extensions_ids)
t.env.globals["EXTENSIONS"] = list(settings.lnbits_installed_extensions_ids)
if settings.lnbits_custom_logo:
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
if settings.lnbits_default_reaction:
t.env.globals["USE_DEFAULT_REACTION"] = settings.lnbits_default_reaction
if settings.lnbits_default_theme:
t.env.globals["USE_DEFAULT_THEME"] = settings.lnbits_default_theme
if settings.lnbits_default_border:
t.env.globals["USE_DEFAULT_BORDER"] = settings.lnbits_default_border
if settings.lnbits_default_gradient:
t.env.globals["USE_DEFAULT_GRADIENT"] = settings.lnbits_default_gradient
if settings.lnbits_default_bgimage:
t.env.globals["USE_DEFAULT_BGIMAGE"] = settings.lnbits_default_bgimage
if settings.bundle_assets:
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
t.env.globals["INCLUDED_CSS"] = ["bundle.min.css"]
@@ -198,27 +220,42 @@ def is_valid_pubkey(pubkey: str) -> bool:
return False
def create_access_token(data: dict):
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.auth_token_expire_minutes
)
to_encode = data.copy()
to_encode.update({"exp": expire})
def create_access_token(data: dict, token_expire_minutes: Optional[int] = None) -> str:
minutes = token_expire_minutes or settings.auth_token_expire_minutes
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
to_encode = {k: v for k, v in data.items() if v is not None}
to_encode.update({"exp": expire}) # todo:check expiration
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"""
def encrypt_internal_message(
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
"""
Encrypt message with the internal secret key
Args:
m: Message to encrypt
urlsafe: Whether to use URL-safe base64 encoding
"""
if not m:
return None
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode())
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
def decrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
"""Decrypt message with the internal secret key"""
def decrypt_internal_message(
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
"""
Decrypt message with the internal secret key
Args:
m: Message to decrypt
urlsafe: Whether the message uses URL-safe base64 encoding
"""
if not m:
return None
return AESCipher(key=settings.auth_secret_key).decrypt(m)
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
@@ -256,6 +293,15 @@ def is_lnbits_version_ok(
return True
def check_callback_url(url: str):
netloc = urlparse(url).netloc
for rule in settings.lnbits_callback_url_rules:
if re.match(rule, netloc) is None:
raise ValueError(
f"Callback not allowed. URL: {url}. Netloc: {netloc}. Rule: {rule}"
)
def download_url(url, save_path):
with request.urlopen(url, timeout=60) as dl_file:
with open(save_path, "wb") as out_file:
@@ -270,3 +316,45 @@ def file_hash(filename):
while n := f.readinto(mv):
h.update(mv[:n])
return h.hexdigest()
def get_api_routes(routes: list) -> dict[str, str]:
data = {}
for route in routes:
if not isinstance(route, APIRoute):
continue
segments = route.path.split("/")
if len(segments) < 3:
continue
if "/".join(segments[1:3]) == "api/v1":
data["/".join(segments[0:4])] = segments[3].capitalize()
elif "/".join(segments[2:4]) == "api/v1":
data["/".join(segments[0:4])] = segments[1].capitalize()
return data
def path_segments(path: str) -> list[str]:
path = path.strip("/")
segments = path.split("/")
if len(segments) < 2:
return segments
if segments[0] == "upgrades":
return segments[2:]
return segments[0:]
def normalize_path(path: Optional[str]) -> str:
path = path or ""
return "/" + "/".join(path_segments(path))
def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
image_folder = Path(settings.lnbits_data_folder, directory)
file_path = image_folder / filename
# Prevent dir traversal attack
if image_folder.resolve() not in file_path.resolve().parents:
raise ValueError("Unsafe filename.")
# Prevent filename with subdirectories
file_path = image_folder / filename.split("/")[-1]
return file_path.resolve()
+10 -5
View File
@@ -15,7 +15,7 @@ from starlette.types import ASGIApp, Receive, Scope, Send
from lnbits.core.db import core_app_extra
from lnbits.core.models import AuditEntry
from lnbits.helpers import template_renderer
from lnbits.helpers import normalize_path, template_renderer
from lnbits.settings import settings
@@ -82,7 +82,11 @@ class InstalledExtensionMiddleware:
return HTMLResponse(
status_code=status_code,
content=template_renderer()
.TemplateResponse(Request(scope), "error.html", {"err": msg})
.TemplateResponse(
Request(scope),
"error.html",
{"err": msg, "status_code": status_code, "message": msg},
)
.body,
)
@@ -114,7 +118,6 @@ class ExtensionsRedirectMiddleware:
class AuditMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp, audit_queue: asyncio.Queue) -> None:
super().__init__(app)
self.audit_queue = audit_queue
@@ -131,8 +134,9 @@ class AuditMiddleware(BaseHTTPMiddleware):
assert response
return response
finally:
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
await self._log_audit(request, response, duration, request_details)
if request_details:
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
await self._log_audit(request, response, duration, request_details)
async def _log_audit(
self,
@@ -156,6 +160,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
if settings.is_super_user(user_id):
user_id = "super_user"
component = "core"
path = normalize_path(path)
if path and not path.startswith("/api"):
component = path.split("/")[1]
+54 -40
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
from pydantic import BaseModel
@@ -15,10 +15,10 @@ if TYPE_CHECKING:
class NodePeerInfo(BaseModel):
id: str
alias: Optional[str] = None
color: Optional[str] = None
last_timestamp: Optional[int] = None
addresses: Optional[list[str]] = None
alias: str | None = None
color: str | None = None
last_timestamp: int | None = None
addresses: list[str] | None = None
class ChannelState(Enum):
@@ -38,22 +38,29 @@ class ChannelPoint(BaseModel):
funding_txid: str
output_index: int
def __str__(self):
return f"{self.funding_txid}:{self.output_index}"
class NodeChannel(BaseModel):
short_id: Optional[str] = None
point: Optional[ChannelPoint] = None
peer_id: str
balance: ChannelBalance
state: ChannelState
name: Optional[str]
color: Optional[str]
# could be optional for closing/pending channels on lndrest
id: str | None = None
short_id: str | None = None
point: ChannelPoint | None = None
name: str | None = None
color: str | None = None
fee_ppm: int | None = None
fee_base_msat: int | None = None
class ChannelStats(BaseModel):
counts: dict[ChannelState, int]
avg_size: int
biggest_size: Optional[int]
smallest_size: Optional[int]
biggest_size: int | None
smallest_size: int | None
total_capacity: int
@classmethod
@@ -88,9 +95,9 @@ class ChannelStats(BaseModel):
class NodeFees(BaseModel):
total_msat: int
daily_msat: Optional[int] = None
weekly_msat: Optional[int] = None
monthly_msat: Optional[int] = None
daily_msat: int | None = None
weekly_msat: int | None = None
monthly_msat: int | None = None
class PublicNodeInfo(BaseModel):
@@ -114,25 +121,25 @@ class NodeInfoResponse(PublicNodeInfo):
class NodePayment(BaseModel):
pending: bool
amount: int
fee: Optional[int] = None
memo: Optional[str] = None
fee: int | None = None
memo: str | None = None
time: int
bolt11: Optional[str] = None
preimage: Optional[str]
bolt11: str | None = None
preimage: str | None
payment_hash: str
expiry: Optional[float] = None
destination: Optional[NodePeerInfo] = None
expiry: float | None = None
destination: NodePeerInfo | None = None
class NodeInvoice(BaseModel):
pending: bool
amount: int
memo: Optional[str]
memo: str | None
bolt11: str
preimage: Optional[str]
preimage: str | None
payment_hash: str
paid_at: Optional[int] = None
expiry: Optional[int] = None
paid_at: int | None = None
expiry: int | None = None
class NodeInvoiceFilters(FilterModel):
@@ -144,11 +151,10 @@ class NodePaymentsFilters(FilterModel):
class Node(ABC):
wallet: Wallet
def __init__(self, wallet: Wallet):
self.wallet = wallet
self.id: Optional[str] = None
self.id: str | None = None
@property
def name(self):
@@ -161,7 +167,7 @@ class Node(ABC):
@abstractmethod
async def _get_id(self) -> str:
pass
raise NotImplementedError
async def get_peers(self) -> list[NodePeerInfo]:
peer_ids = await self.get_peer_ids()
@@ -169,15 +175,15 @@ class Node(ABC):
@abstractmethod
async def get_peer_ids(self) -> list[str]:
pass
raise NotImplementedError
@abstractmethod
async def connect_peer(self, uri: str):
pass
raise NotImplementedError
@abstractmethod
async def disconnect_peer(self, peer_id: str):
pass
raise NotImplementedError
@abstractmethod
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
@@ -197,27 +203,35 @@ class Node(ABC):
self,
peer_id: str,
local_amount: int,
push_amount: Optional[int] = None,
fee_rate: Optional[int] = None,
push_amount: int | None = None,
fee_rate: int | None = None,
) -> ChannelPoint:
pass
raise NotImplementedError
@abstractmethod
async def close_channel(
self,
short_id: Optional[str] = None,
point: Optional[ChannelPoint] = None,
short_id: str | None = None,
point: ChannelPoint | None = None,
force: bool = False,
):
pass
raise NotImplementedError
@abstractmethod
async def get_channel(self, channel_id: str) -> NodeChannel | None:
raise NotImplementedError
@abstractmethod
async def get_channels(self) -> list[NodeChannel]:
pass
raise NotImplementedError
@abstractmethod
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
raise NotImplementedError
@abstractmethod
async def get_info(self) -> NodeInfoResponse:
pass
raise NotImplementedError
async def get_public_info(self) -> PublicNodeInfo:
info = await self.get_info()
@@ -227,10 +241,10 @@ class Node(ABC):
async def get_payments(
self, filters: Filters[NodePaymentsFilters]
) -> Page[NodePayment]:
pass
raise NotImplementedError
@abstractmethod
async def get_invoices(
self, filters: Filters[NodeInvoiceFilters]
) -> Page[NodeInvoice]:
pass
raise NotImplementedError
+57 -33
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio
from http import HTTPStatus
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
from fastapi import HTTPException
@@ -66,6 +66,28 @@ class CoreLightningNode(Node):
fn = getattr(self.wallet.ln, method)
return await loop.run_in_executor(None, lambda: fn(*args, **kwargs))
def _parse_state(self, state: str) -> ChannelState:
if state == "CHANNELD_NORMAL":
return ChannelState.ACTIVE
if state in (
# wait for force close
"AWAITING_UNILATERAL",
# waiting for close
"CHANNELD_SHUTTING_DOWN",
# waiting for open
"CHANNELD_AWAITING_LOCKIN",
"OPENINGD",
):
return ChannelState.PENDING
if state in (
"CHANNELD_CLOSING",
"CLOSINGD_COMPLETE",
"CLOSINGD_SIGEXCHANGE",
"ONCHAIN",
):
return ChannelState.CLOSED
return ChannelState.INACTIVE
@catch_rpc_errors
async def connect_peer(self, uri: str):
# https://docs.corelightning.org/reference/lightning-connect
@@ -97,8 +119,8 @@ class CoreLightningNode(Node):
self,
peer_id: str,
local_amount: int,
push_amount: Optional[int] = None,
fee_rate: Optional[int] = None,
push_amount: int | None = None,
fee_rate: int | None = None,
) -> ChannelPoint:
try:
result = await self.ln_rpc(
@@ -151,8 +173,8 @@ class CoreLightningNode(Node):
@catch_rpc_errors
async def close_channel(
self,
short_id: Optional[str] = None,
point: Optional[ChannelPoint] = None,
short_id: str | None = None,
point: ChannelPoint | None = None,
force: bool = False,
):
if not short_id:
@@ -202,48 +224,45 @@ class CoreLightningNode(Node):
else:
return NodePeerInfo(id=node["nodeid"])
@catch_rpc_errors
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
await self.ln_rpc("setchannel", channel_id, feebase=base_msat, feeppm=ppm)
@catch_rpc_errors
async def get_channel(self, channel_id: str) -> NodeChannel | None:
channels = await self.get_channels()
for channel in channels:
if channel.id == channel_id:
return channel
return None
@catch_rpc_errors
async def get_channels(self) -> list[NodeChannel]:
funds = await self.ln_rpc("listfunds")
channels = await self.ln_rpc("listpeerchannels")
nodes = await self.ln_rpc("listnodes")
nodes_by_id = {n["nodeid"]: n for n in nodes["nodes"]}
return [
NodeChannel(
id=ch["channel_id"],
short_id=ch.get("short_channel_id"),
point=ChannelPoint(
funding_txid=ch["funding_txid"],
output_index=ch["funding_output"],
output_index=ch["funding_outnum"],
),
peer_id=ch["peer_id"],
balance=ChannelBalance(
local_msat=ch["our_amount_msat"],
remote_msat=ch["amount_msat"] - ch["our_amount_msat"],
total_msat=ch["amount_msat"],
local_msat=ch["spendable_msat"],
remote_msat=ch["receivable_msat"],
total_msat=ch["total_msat"],
),
fee_ppm=ch["fee_proportional_millionths"],
fee_base_msat=ch["fee_base_msat"],
name=nodes_by_id.get(ch["peer_id"], {}).get("alias"),
color=nodes_by_id.get(ch["peer_id"], {}).get("color"),
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.INACTIVE
)
)
),
state=self._parse_state(ch["state"]),
)
for ch in funds["channels"]
for ch in channels["channels"]
]
@catch_rpc_errors
@@ -285,14 +304,19 @@ class CoreLightningNode(Node):
return [
NodePayment(
bolt11=pay.get("bolt11"),
amount=pay["amount_msat"],
fee=int(pay["amount_msat"]) - int(pay["amount_sent_msat"]),
amount=pay.get("amount_msat", 0),
fee=int(pay.get("amount_msat", 0))
- int(pay.get("amount_sent_msat", 0)),
memo=pay.get("description"),
time=pay["created_at"],
preimage=pay.get("preimage"),
payment_hash=pay["payment_hash"],
pending=pay["status"] != "complete",
destination=await self.get_peer_info(pay["destination"]),
destination=(
await self.get_peer_info(pay.get("destination"))
if pay.get("destination")
else None
),
)
for pay in reversed(result["pays"])
if pay["status"] != "failed"
+98 -17
View File
@@ -4,7 +4,7 @@ import asyncio
import base64
import json
from http import HTTPStatus
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
from fastapi import HTTPException
from httpx import HTTPStatusError
@@ -41,6 +41,14 @@ def _decode_bytes(data: str) -> str:
return base64.b64decode(data).hex()
def _encode_bytes(data: str) -> str:
return base64.b64encode(bytes.fromhex(data)).decode()
def _encode_urlsafe_bytes(data: str) -> str:
return base64.urlsafe_b64encode(bytes.fromhex(data)).decode()
def _parse_channel_point(raw: str) -> ChannelPoint:
funding_tx, output_index = raw.split(":")
return ChannelPoint(
@@ -52,9 +60,7 @@ def _parse_channel_point(raw: str) -> ChannelPoint:
class LndRestNode(Node):
wallet: LndRestWallet
async def request(
self, method: str, path: str, json: Optional[dict] = None, **kwargs
):
async def request(self, method: str, path: str, json: dict | None = None, **kwargs):
response = await self.wallet.client.request(
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
)
@@ -123,21 +129,18 @@ class LndRestNode(Node):
self,
peer_id: str,
local_amount: int,
push_amount: Optional[int] = None,
fee_rate: Optional[int] = None,
push_amount: int | None = None,
fee_rate: int | None = None,
) -> ChannelPoint:
response = await self.request(
"POST",
"/v1/channels",
data=json.dumps(
{
# 'node_pubkey': base64.b64encode(peer_id.encode()).decode(),
"node_pubkey_string": peer_id,
"sat_per_vbyte": fee_rate,
"local_funding_amount": local_amount,
"push_sat": push_amount,
}
),
json={
"node_pubkey": _encode_bytes(peer_id),
"sat_per_vbyte": fee_rate,
"local_funding_amount": local_amount,
"push_sat": push_amount,
},
)
return ChannelPoint(
# WHY IS THIS REVERSED?!
@@ -171,8 +174,8 @@ class LndRestNode(Node):
async def close_channel(
self,
short_id: Optional[str] = None,
point: Optional[ChannelPoint] = None,
short_id: str | None = None,
point: ChannelPoint | None = None,
force: bool = False,
):
if short_id:
@@ -184,6 +187,81 @@ class LndRestNode(Node):
asyncio.create_task(self._close_channel(point, force)) # noqa: RUF006
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
# https://lightning.engineering/api-docs/api/lnd/lightning/update-channel-policy/
channel = await self.get_channel(channel_id)
if not channel:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Channel not found"
)
if not channel.point:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Channel point required"
)
await self.request(
"POST",
"/v1/chanpolicy",
json={
"base_fee_msat": base_msat,
"fee_rate_ppm": ppm,
"chan_point": {
"funding_txid_str": channel.point.funding_txid,
"output_index": channel.point.output_index,
},
# https://docs.lightning.engineering/lightning-network-tools/lnd/optimal-configuration-of-a-routing-node#channel-defaults
"time_lock_delta": 80,
# 'max_htlc_msat': <uint64>,
# 'min_htlc_msat': <uint64>,
# 'inbound_fee': <InboundFee>,
},
)
async def get_channel(self, channel_id: str) -> NodeChannel | None:
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
info = await self.get("/v1/getinfo")
if info["identity_pubkey"] == channel_info["node1_pub"]:
my_node_key = "node1"
peer_node_key = "node2"
else:
my_node_key = "node2"
peer_node_key = "node1"
peer_id = channel_info[f"{peer_node_key}_pub"]
peer_b64 = _encode_urlsafe_bytes(peer_id)
channels = await self.get(f"/v1/channels?peer={peer_b64}")
if "error" in channel_info and "error" in channels:
logger.debug("LND get_channel", channels)
return None
if len(channels["channels"]) == 0:
logger.debug(f"LND get_channel no channels founds with id {peer_b64}")
return None
for channel in channels["channels"]:
if channel["chan_id"] == channel_id:
peer_info = await self.get_peer_info(peer_id)
return NodeChannel(
id=channel.get("chan_id"),
peer_id=peer_info.id,
name=peer_info.alias,
color=peer_info.color,
state=(
ChannelState.ACTIVE
if channel["active"]
else ChannelState.INACTIVE
),
fee_ppm=channel_info[f"{my_node_key}_policy"][
"fee_rate_milli_msat"
],
fee_base_msat=channel_info[f"{my_node_key}_policy"][
"fee_base_msat"
],
point=_parse_channel_point(channel["channel_point"]),
balance=ChannelBalance(
local_msat=msat(channel["local_balance"]),
remote_msat=msat(channel["remote_balance"]),
total_msat=msat(channel["capacity"]),
),
)
return None
async def get_channels(self) -> list[NodeChannel]:
normal, pending, closed = await asyncio.gather(
self.get("/v1/channels"),
@@ -203,6 +281,7 @@ class LndRestNode(Node):
state=state,
name=info.alias,
color=info.color,
id=channel.get("chan_id", "node is for pending channels"),
point=_parse_channel_point(channel["channel_point"]),
balance=ChannelBalance(
local_msat=msat(channel["local_balance"]),
@@ -222,6 +301,7 @@ class LndRestNode(Node):
info = await self.get_peer_info(channel["remote_pubkey"])
channels.append(
NodeChannel(
id=channel.get("chan_id", "node is for closing channels"),
peer_id=info.id,
state=ChannelState.CLOSED,
name=info.alias,
@@ -239,6 +319,7 @@ class LndRestNode(Node):
info = await self.get_peer_info(channel["remote_pubkey"])
channels.append(
NodeChannel(
id=channel["chan_id"],
short_id=channel["chan_id"],
point=_parse_channel_point(channel["channel_point"]),
peer_id=channel["remote_pubkey"],
+216 -168
View File
@@ -4,15 +4,16 @@ import importlib
import importlib.metadata
import inspect
import json
import os
import re
from datetime import datetime, timezone
from enum import Enum
from hashlib import sha256
from os import path
from time import time
from typing import Any, Optional
from pathlib import Path
from time import gmtime, strftime, time
from typing import Any
from uuid import uuid4
import httpx
from loguru import logger
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
@@ -76,7 +77,7 @@ class RedirectPath(BaseModel):
other.from_path, list(other.header_filters.items())
) or other.redirect_matches(self.from_path, list(self.header_filters.items()))
def find_in_conflict(self, others: list[RedirectPath]) -> Optional[RedirectPath]:
def find_in_conflict(self, others: list[RedirectPath]) -> RedirectPath | None:
for other in others:
if self.in_conflict(other):
return other
@@ -148,11 +149,11 @@ class InstalledExtensionsSettings(LNbitsSettings):
lnbits_extensions_redirects: list[RedirectPath] = Field(default=[])
# list of all extension ids
lnbits_all_extensions_ids: set[str] = Field(default=[])
lnbits_installed_extensions_ids: set[str] = Field(default=[])
def find_extension_redirect(
self, path: str, req_headers: list[tuple[bytes, bytes]]
) -> Optional[RedirectPath]:
) -> RedirectPath | None:
headers = [(k.decode(), v.decode()) for k, v in req_headers]
return next(
(
@@ -166,8 +167,8 @@ class InstalledExtensionsSettings(LNbitsSettings):
def activate_extension_paths(
self,
ext_id: str,
upgrade_hash: Optional[str] = None,
ext_redirects: Optional[list[dict]] = None,
upgrade_hash: str | None = None,
ext_redirects: list[dict] | None = None,
):
self.lnbits_deactivated_extensions.discard(ext_id)
@@ -181,7 +182,7 @@ class InstalledExtensionsSettings(LNbitsSettings):
if ext_redirects:
self._activate_extension_redirects(ext_id, ext_redirects)
self.lnbits_all_extensions_ids.add(ext_id)
self.lnbits_installed_extensions_ids.add(ext_id)
def deactivate_extension_paths(self, ext_id: str):
self.lnbits_deactivated_extensions.add(ext_id)
@@ -215,7 +216,6 @@ class InstalledExtensionsSettings(LNbitsSettings):
class ExchangeHistorySettings(LNbitsSettings):
lnbits_exchange_rate_history: list[dict] = Field(default=[])
def append_exchange_rate_datapoint(self, rates: dict, max_size: int):
@@ -231,12 +231,12 @@ class ExchangeHistorySettings(LNbitsSettings):
class ThemesSettings(LNbitsSettings):
lnbits_site_title: str = Field(default="LNbits")
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
lnbits_site_description: Optional[str] = Field(
lnbits_site_description: str | None = Field(
default="The world's most powerful suite of bitcoin tools."
)
lnbits_show_home_page_elements: bool = Field(default=True)
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
lnbits_custom_badge: Optional[str] = Field(default=None)
lnbits_custom_badge: str | None = Field(default=None)
lnbits_custom_badge_color: str = Field(default="warning")
lnbits_theme_options: list[str] = Field(
default=[
@@ -247,33 +247,56 @@ class ThemesSettings(LNbitsSettings):
"monochrome",
"autumn",
"cyber",
"flamingo",
"bitcoin",
]
)
lnbits_custom_logo: Optional[str] = Field(default=None)
lnbits_custom_logo: str | None = Field(default=None)
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
lnbits_ad_space_title: str = Field(default="Supported by")
lnbits_ad_space: str = Field(
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
) # sneaky sneaky
lnbits_ad_space_enabled: bool = Field(default=False)
lnbits_allowed_currencies: list[str] = Field(default=[])
lnbits_default_accounting_currency: Optional[str] = Field(default=None)
lnbits_qr_logo: str = Field(default="/static/images/logos/lnbits.png")
lnbits_default_accounting_currency: str | None = Field(default=None)
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
lnbits_default_reaction: str = Field(default="confettiBothSides")
lnbits_default_theme: str = Field(default="salvador")
lnbits_default_border: str = Field(default="hard-border")
lnbits_default_gradient: bool = Field(default=True)
lnbits_default_bgimage: str = Field(default=None)
class OpsSettings(LNbitsSettings):
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
lnbits_hide_api: bool = Field(default=False)
lnbits_denomination: str = Field(default="sats")
lnbits_upload_size_bytes: int = Field(default=512_000, ge=0) # 500kb
lnbits_upload_allowed_types: list[str] = Field(
default=[
"image/png",
"image/jpeg",
"image/jpg",
"image/heic",
"image/heif",
"image/heics",
"png",
"jpeg",
"jpg",
"heic",
"heif",
"heics",
]
)
class FeeSettings(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_reserve_fee_min: int = Field(default=2000, ge=0)
lnbits_reserve_fee_percent: float = Field(default=1.0, ge=0)
lnbits_service_fee: float = Field(default=0, ge=0)
lnbits_service_fee_ignore_internal: bool = Field(default=True)
lnbits_service_fee_max: int = Field(default=0)
lnbits_service_fee_wallet: Optional[str] = Field(default=None)
lnbits_service_fee_wallet: str | None = Field(default=None)
# WARN: this same value must be used for balance check and passed to
# funding_source.pay_invoice(), it may cause a vulnerability if the values differ
@@ -286,9 +309,9 @@ class FeeSettings(LNbitsSettings):
class ExchangeProvidersSettings(LNbitsSettings):
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
lnbits_exchange_history_size: int = Field(default=60)
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0)
lnbits_exchange_history_size: int = Field(default=60, ge=0)
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
default=[
@@ -334,13 +357,6 @@ class ExchangeProvidersSettings(LNbitsSettings):
exclude_to=[],
ticker_conversion=[],
),
ExchangeRateProvider(
name="CoinMate",
api_url="https://coinmate.io/api/ticker?currencyPair=BTC_{TO}",
path="$.data.last",
exclude_to=[],
ticker_conversion=["USD:USDT"],
),
ExchangeRateProvider(
name="Kraken",
api_url="https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
@@ -360,25 +376,25 @@ class ExchangeProvidersSettings(LNbitsSettings):
class SecuritySettings(LNbitsSettings):
lnbits_rate_limit_no: str = Field(default="200")
lnbits_rate_limit_no: int = Field(default=200, ge=0)
lnbits_rate_limit_unit: str = Field(default="minute")
lnbits_allowed_ips: list[str] = Field(default=[])
lnbits_blocked_ips: list[str] = Field(default=[])
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)
lnbits_status_manifest: str = Field(
default=(
"https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
)
lnbits_callback_url_rules: list[str] = Field(
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
)
lnbits_wallet_limit_max_balance: int = Field(default=0, ge=0)
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0, ge=0)
lnbits_wallet_limit_secs_between_trans: int = Field(default=0, ge=0)
lnbits_only_allow_incoming_payments: bool = Field(default=False)
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
lnbits_watchdog_interval_minutes: int = Field(default=60, gt=0)
lnbits_watchdog_delta: int = Field(default=1_000_000, gt=0)
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
def is_wallet_max_balance_exceeded(self, amount):
return (
self.lnbits_wallet_limit_max_balance
@@ -387,130 +403,161 @@ class SecuritySettings(LNbitsSettings):
)
class NotificationsSettings(LNbitsSettings):
lnbits_nostr_notifications_enabled: bool = Field(default=False)
lnbits_nostr_notifications_private_key: str = Field(default="")
lnbits_nostr_notifications_identifiers: list[str] = Field(default=[])
lnbits_telegram_notifications_enabled: bool = Field(default=False)
lnbits_telegram_notifications_access_token: str = Field(default="")
lnbits_telegram_notifications_chat_id: str = Field(default="")
lnbits_email_notifications_enabled: bool = Field(default=False)
lnbits_email_notifications_email: str = Field(default="")
lnbits_email_notifications_username: str = Field(default="")
lnbits_email_notifications_password: str = Field(default="")
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
lnbits_email_notifications_port: int = Field(default=587)
lnbits_email_notifications_to_emails: list[str] = Field(default=[])
lnbits_notification_settings_update: bool = Field(default=True)
lnbits_notification_credit_debit: bool = Field(default=True)
notification_balance_delta_changed: bool = Field(default=True)
lnbits_notification_server_start_stop: bool = Field(default=True)
lnbits_notification_watchdog: bool = Field(default=False)
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
lnbits_notification_incoming_payment_amount_sats: int = Field(
default=1_000_000, ge=0
)
lnbits_notification_outgoing_payment_amount_sats: int = Field(
default=1_000_000, ge=0
)
class FakeWalletFundingSource(LNbitsSettings):
fake_wallet_secret: str = Field(default="ToTheMoon1")
lnbits_denomination: str = Field(default="sats")
class LNbitsFundingSource(LNbitsSettings):
lnbits_endpoint: str = Field(default="https://demo.lnbits.com")
lnbits_key: Optional[str] = Field(default=None)
lnbits_admin_key: Optional[str] = Field(default=None)
lnbits_invoice_key: Optional[str] = Field(default=None)
lnbits_key: str | None = Field(default=None)
lnbits_admin_key: str | None = Field(default=None)
lnbits_invoice_key: str | None = Field(default=None)
class ClicheFundingSource(LNbitsSettings):
cliche_endpoint: Optional[str] = Field(default=None)
cliche_endpoint: str | None = Field(default=None)
class CoreLightningFundingSource(LNbitsSettings):
corelightning_rpc: Optional[str] = Field(default=None)
corelightning_rpc: str | None = Field(default=None)
corelightning_pay_command: str = Field(default="pay")
clightning_rpc: Optional[str] = Field(default=None)
clightning_rpc: str | None = Field(default=None)
class CoreLightningRestFundingSource(LNbitsSettings):
corelightning_rest_url: Optional[str] = Field(default=None)
corelightning_rest_macaroon: Optional[str] = Field(default=None)
corelightning_rest_cert: Optional[str] = Field(default=None)
corelightning_rest_url: str | None = Field(default=None)
corelightning_rest_macaroon: str | None = Field(default=None)
corelightning_rest_cert: str | None = Field(default=None)
class EclairFundingSource(LNbitsSettings):
eclair_url: Optional[str] = Field(default=None)
eclair_pass: Optional[str] = Field(default=None)
eclair_url: str | None = Field(default=None)
eclair_pass: str | None = Field(default=None)
class LndRestFundingSource(LNbitsSettings):
lnd_rest_endpoint: Optional[str] = Field(default=None)
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_endpoint: str | None = Field(default=None)
lnd_rest_cert: str | None = Field(default=None)
lnd_rest_macaroon: str | None = Field(default=None)
lnd_rest_macaroon_encrypted: str | None = Field(default=None)
lnd_rest_route_hints: bool = Field(default=True)
lnd_rest_allow_self_payment: bool = Field(default=False)
lnd_cert: Optional[str] = Field(default=None)
lnd_admin_macaroon: Optional[str] = Field(default=None)
lnd_invoice_macaroon: Optional[str] = Field(default=None)
lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
lnd_cert: str | None = Field(default=None)
lnd_admin_macaroon: str | None = Field(default=None)
lnd_invoice_macaroon: str | None = Field(default=None)
lnd_rest_admin_macaroon: str | None = Field(default=None)
lnd_rest_invoice_macaroon: str | None = Field(default=None)
class LndGrpcFundingSource(LNbitsSettings):
lnd_grpc_endpoint: Optional[str] = Field(default=None)
lnd_grpc_cert: Optional[str] = Field(default=None)
lnd_grpc_port: Optional[int] = Field(default=None)
lnd_grpc_admin_macaroon: Optional[str] = Field(default=None)
lnd_grpc_invoice_macaroon: Optional[str] = Field(default=None)
lnd_grpc_macaroon: Optional[str] = Field(default=None)
lnd_grpc_macaroon_encrypted: Optional[str] = Field(default=None)
lnd_grpc_endpoint: str | None = Field(default=None)
lnd_grpc_cert: str | None = Field(default=None)
lnd_grpc_port: int | None = Field(default=None)
lnd_grpc_admin_macaroon: str | None = Field(default=None)
lnd_grpc_invoice_macaroon: str | None = Field(default=None)
lnd_grpc_macaroon: str | None = Field(default=None)
lnd_grpc_macaroon_encrypted: str | None = Field(default=None)
class LnPayFundingSource(LNbitsSettings):
lnpay_api_endpoint: Optional[str] = Field(default=None)
lnpay_api_key: Optional[str] = Field(default=None)
lnpay_wallet_key: Optional[str] = Field(default=None)
lnpay_admin_key: Optional[str] = Field(default=None)
lnpay_api_endpoint: str | None = Field(default=None)
lnpay_api_key: str | None = Field(default=None)
lnpay_wallet_key: str | None = Field(default=None)
lnpay_admin_key: str | None = Field(default=None)
class BlinkFundingSource(LNbitsSettings):
blink_api_endpoint: Optional[str] = Field(default="https://api.blink.sv/graphql")
blink_ws_endpoint: Optional[str] = Field(default="wss://ws.blink.sv/graphql")
blink_token: Optional[str] = Field(default=None)
blink_api_endpoint: str | None = Field(default="https://api.blink.sv/graphql")
blink_ws_endpoint: str | None = Field(default="wss://ws.blink.sv/graphql")
blink_token: str | None = 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)
zbd_api_endpoint: str | None = Field(default="https://api.zebedee.io/v0/")
zbd_api_key: str | None = Field(default=None)
class PhoenixdFundingSource(LNbitsSettings):
phoenixd_api_endpoint: Optional[str] = Field(default="http://localhost:9740/")
phoenixd_api_password: Optional[str] = Field(default=None)
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
phoenixd_api_password: str | None = 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)
alby_api_endpoint: str | None = Field(default="https://api.getalby.com/")
alby_access_token: str | None = Field(default=None)
class OpenNodeFundingSource(LNbitsSettings):
opennode_api_endpoint: Optional[str] = Field(default=None)
opennode_key: Optional[str] = Field(default=None)
opennode_admin_key: Optional[str] = Field(default=None)
opennode_invoice_key: Optional[str] = Field(default=None)
opennode_api_endpoint: str | None = Field(default=None)
opennode_key: str | None = Field(default=None)
opennode_admin_key: str | None = Field(default=None)
opennode_invoice_key: str | None = Field(default=None)
class SparkFundingSource(LNbitsSettings):
spark_url: Optional[str] = Field(default=None)
spark_token: Optional[str] = Field(default=None)
spark_url: str | None = Field(default=None)
spark_token: str | None = Field(default=None)
class LnTipsFundingSource(LNbitsSettings):
lntips_api_endpoint: Optional[str] = Field(default=None)
lntips_api_key: Optional[str] = Field(default=None)
lntips_admin_key: Optional[str] = Field(default=None)
lntips_invoice_key: Optional[str] = Field(default=None)
lntips_api_endpoint: str | None = Field(default=None)
lntips_api_key: str | None = Field(default=None)
lntips_admin_key: str | None = Field(default=None)
lntips_invoice_key: str | None = Field(default=None)
class NWCFundingSource(LNbitsSettings):
nwc_pairing_url: Optional[str] = Field(default=None)
nwc_pairing_url: str | None = Field(default=None)
class BreezSdkFundingSource(LNbitsSettings):
breez_api_key: Optional[str] = Field(default=None)
breez_greenlight_seed: Optional[str] = Field(default=None)
breez_greenlight_invite_code: Optional[str] = Field(default=None)
breez_greenlight_device_key: Optional[str] = Field(default=None)
breez_greenlight_device_cert: Optional[str] = Field(default=None)
breez_api_key: str | None = Field(default=None)
breez_greenlight_seed: str | None = Field(default=None)
breez_greenlight_invite_code: str | None = Field(default=None)
breez_greenlight_device_key: str | None = Field(default=None)
breez_greenlight_device_cert: str | None = Field(default=None)
breez_use_trampoline: bool = Field(default=True)
class BoltzFundingSource(LNbitsSettings):
boltz_client_endpoint: Optional[str] = Field(default="127.0.0.1:9002")
boltz_client_macaroon: Optional[str] = Field(default=None)
boltz_client_wallet: Optional[str] = Field(default="lnbits")
boltz_client_cert: Optional[str] = Field(default=None)
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
boltz_client_macaroon: str | None = Field(default=None)
boltz_client_wallet: str | None = Field(default="lnbits")
boltz_client_cert: str | None = Field(default=None)
class LightningSettings(LNbitsSettings):
lightning_invoice_expiry: int = Field(default=3600)
lightning_invoice_expiry: int = Field(default=3600, gt=0)
class FundingSourcesSettings(
@@ -535,11 +582,14 @@ class FundingSourcesSettings(
BreezSdkFundingSource,
):
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
# How long to wait for the payment to be confirmed before returning a pending status
# It will not fail the payment, it will make it return pending after the timeout
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
class WebPushSettings(LNbitsSettings):
lnbits_webpush_pubkey: Optional[str] = Field(default=None)
lnbits_webpush_privkey: Optional[str] = Field(default=None)
lnbits_webpush_pubkey: str | None = Field(default=None)
lnbits_webpush_privkey: str | None = Field(default=None)
class NodeUISettings(LNbitsSettings):
@@ -573,7 +623,7 @@ class AuthMethods(Enum):
class AuthSettings(LNbitsSettings):
auth_token_expire_minutes: int = Field(default=525600)
auth_token_expire_minutes: int = Field(default=525600, gt=0)
auth_all_methods = [a.value for a in AuthMethods]
auth_allowed_methods: list[str] = Field(
default=[
@@ -583,7 +633,7 @@ class AuthSettings(LNbitsSettings):
)
# How many seconds after login the user is allowed to update its credentials.
# A fresh login is required afterwards.
auth_credetials_update_threshold: int = Field(default=120)
auth_credetials_update_threshold: int = Field(default=120, gt=0)
def is_auth_method_allowed(self, method: AuthMethods):
return method.value in self.auth_allowed_methods
@@ -615,7 +665,7 @@ class AuditSettings(LNbitsSettings):
lnbits_audit_enabled: bool = Field(default=True)
# number of days to keep the audit entry
lnbits_audit_retention_days: int = Field(default=7)
lnbits_audit_retention_days: int = Field(default=7, ge=0)
lnbits_audit_log_ip_address: bool = Field(default=False)
lnbits_audit_log_path_params: bool = Field(default=True)
@@ -645,9 +695,9 @@ class AuditSettings(LNbitsSettings):
def audit_http_request(
self,
http_method: Optional[str] = None,
path: Optional[str] = None,
http_response_code: Optional[str] = None,
http_method: str | None = None,
path: str | None = None,
http_response_code: str | None = None,
) -> bool:
if not self.lnbits_audit_enabled:
return False
@@ -665,24 +715,24 @@ class AuditSettings(LNbitsSettings):
return True
def _is_http_request_path_auditable(self, path: Optional[str]):
def _is_http_request_path_auditable(self, path: str | None):
if len(self.lnbits_audit_exclude_paths) != 0 and path:
for exclude_path in self.lnbits_audit_exclude_paths:
if _re_fullmatch_safe(exclude_path, path):
return False
if len(self.lnbits_audit_include_paths) != 0:
if not path:
return False
for include_path in self.lnbits_audit_include_paths:
if _re_fullmatch_safe(include_path, path):
return True
if len(self.lnbits_audit_include_paths) == 0:
return True
if not path:
return False
for include_path in self.lnbits_audit_include_paths:
if _re_fullmatch_safe(include_path, path):
return True
return False
def _is_http_response_code_auditable(
self, http_response_code: Optional[str]
) -> bool:
def _is_http_response_code_auditable(self, http_response_code: str | None) -> bool:
if not http_response_code:
# No response code means only request filters should apply
return True
@@ -705,6 +755,7 @@ class EditableSettings(
FeeSettings,
ExchangeProvidersSettings,
SecuritySettings,
NotificationsSettings,
FundingSourcesSettings,
LightningSettings,
WebPushSettings,
@@ -751,7 +802,7 @@ class EnvSettings(LNbitsSettings):
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)
port: int = Field(default=5000, gt=0)
forwarded_allow_ips: str = Field(default="*")
lnbits_title: str = Field(default="LNbits API")
lnbits_path: str = Field(default=".")
@@ -763,19 +814,27 @@ class EnvSettings(LNbitsSettings):
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())
cleanup_wallets_days: int = Field(default=90)
funding_source_max_retries: int = Field(default=4)
cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
@property
def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits"
class SaaSSettings(LNbitsSettings):
lnbits_saas_callback: Optional[str] = Field(default=None)
lnbits_saas_secret: Optional[str] = Field(default=None)
lnbits_saas_instance_id: Optional[str] = Field(default=None)
def check_auth_secret_key(self):
if self.auth_secret_key:
return
if not os.path.isdir(settings.lnbits_data_folder):
os.mkdir(settings.lnbits_data_folder)
auth_key_file = Path(settings.lnbits_data_folder, ".lnbits_auth_key")
if auth_key_file.is_file():
with open(auth_key_file) as file:
self.auth_secret_key = file.readline()
return
self.auth_secret_key = uuid4().hex
with open(auth_key_file, "w+") as file:
file.write(self.auth_secret_key)
class PersistenceSettings(LNbitsSettings):
@@ -822,6 +881,18 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
# Long running while loops should use this flag instead of `while True:`
lnbits_running: bool = Field(default=True)
# Remember the latest balance delta in order to compare with the current one
latest_balance_delta_sats: int = Field(default=None)
lnbits_all_extensions_ids: set[str] = Field(default=[])
server_startup_time: int = Field(default=time())
@property
def lnbits_server_up_time(self) -> str:
up_time = int(time() - self.server_startup_time)
return strftime("%H:%M:%S", gmtime(up_time))
@classmethod
def readonly_fields(cls):
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
@@ -830,12 +901,15 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
class ReadOnlySettings(
EnvSettings,
ExtensionsInstallSettings,
SaaSSettings,
PersistenceSettings,
SuperUserSettings,
):
lnbits_admin_ui: bool = Field(default=True)
@property
def lnbits_extensions_upgrade_path(self) -> str:
return str(Path(self.lnbits_data_folder, "upgrades"))
@validator(
"lnbits_allowed_funding_sources",
pre=True,
@@ -864,7 +938,7 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
or user_id == self.super_user
)
def is_super_user(self, user_id: Optional[str] = None) -> bool:
def is_super_user(self, user_id: str | None = None) -> bool:
return user_id == self.super_user
def is_admin_user(self, user_id: str) -> bool:
@@ -873,8 +947,8 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
def is_admin_extension(self, ext_id: str) -> bool:
return ext_id in self.lnbits_admin_extensions
def is_extension_id(self, ext_id: str) -> bool:
return ext_id in self.lnbits_all_extensions_ids
def is_installed_extension_id(self, ext_id: str) -> bool:
return ext_id in self.lnbits_installed_extensions_ids
class SuperSettings(EditableSettings):
@@ -883,12 +957,12 @@ class SuperSettings(EditableSettings):
class AdminSettings(EditableSettings):
is_super_user: bool
lnbits_allowed_funding_sources: Optional[list[str]]
lnbits_allowed_funding_sources: list[str] | None
class SettingsField(BaseModel):
id: str
value: Optional[Any]
value: Any | None
tag: str = "core"
@@ -905,31 +979,6 @@ def set_cli_settings(**kwargs):
setattr(settings, key, value)
def send_admin_user_to_saas():
if settings.lnbits_saas_callback:
with httpx.Client() as client:
headers = {
"Content-Type": "application/json; charset=utf-8",
"X-API-KEY": settings.lnbits_saas_secret,
}
payload = {
"instance_id": settings.lnbits_saas_instance_id,
"adminuser": settings.super_user,
}
try:
client.post(
settings.lnbits_saas_callback,
headers=headers,
json=payload,
)
logger.success("sent super_user to saas application")
except Exception as e:
logger.error(
"error sending super_user to saas:"
f" {settings.lnbits_saas_callback}. Error: {e!s}"
)
readonly_variables = ReadOnlySettings.readonly_fields()
transient_variables = TransientSettings.readonly_fields()
@@ -938,9 +987,8 @@ 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()
)
settings.check_auth_secret_key()
if not settings.user_agent:
settings.user_agent = f"LNbits/{settings.version}"
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+13 -11
View File
File diff suppressed because one or more lines are too long
+174 -7
View File
@@ -1,3 +1,11 @@
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
[data-theme=classic] .q-drawer--dark,
body[data-theme=classic].body--dark,
[data-theme=classic] .q-menu--dark {
@@ -10,11 +18,23 @@ body[data-theme='classic'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
[data-theme=classic] .q-card--dark,
[data-theme=classic] .q-stepper--dark {
background: #333646 !important;
}
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
body[data-theme=classic].body--light {
background-color: #f4f4f4;
}
[data-theme=classic] .bg-primary {
background: #673ab7 !important;
}
@@ -52,6 +72,14 @@ body[data-theme='classic'].body--dark {
color: #fff !important;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .q-drawer--dark,
body[data-theme=bitcoin].body--dark,
[data-theme=bitcoin] .q-menu--dark {
@@ -64,22 +92,34 @@ body[data-theme='bitcoin'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .q-card--dark,
[data-theme=bitcoin] .q-stepper--dark {
background: #333646 !important;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
body[data-theme=bitcoin].body--light {
background-color: #f4f4f4;
}
[data-theme=bitcoin] .bg-primary {
background: #ff9853 !important;
background: #ea611d !important;
}
[data-theme=bitcoin] .text-primary {
color: #ff9853 !important;
color: #ea611d !important;
}
[data-theme=bitcoin] .bg-secondary {
background: #ff7353 !important;
background: #e56f35 !important;
}
[data-theme=bitcoin] .text-secondary {
color: #ff7353 !important;
color: #e56f35 !important;
}
[data-theme=bitcoin] .bg-dark {
background: #2d293b !important;
@@ -94,10 +134,10 @@ body[data-theme='bitcoin'].body--dark {
color: #333646 !important;
}
[data-theme=bitcoin] .bg-marginal-bg {
background: #2d293b !important;
background: #000000 !important;
}
[data-theme=bitcoin] .text-marginal-bg {
color: #2d293b !important;
color: #000000 !important;
}
[data-theme=bitcoin] .bg-marginal-text {
background: #fff !important;
@@ -106,6 +146,14 @@ body[data-theme='bitcoin'].body--dark {
color: #fff !important;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .q-drawer--dark,
body[data-theme=freedom].body--dark,
[data-theme=freedom] .q-menu--dark {
@@ -118,11 +166,23 @@ body[data-theme='freedom'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .q-card--dark,
[data-theme=freedom] .q-stepper--dark {
background: #1b1b1b !important;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
body[data-theme=freedom].body--light {
background-color: #f4f4f4;
}
[data-theme=freedom] .bg-primary {
background: #e22156 !important;
}
@@ -160,6 +220,14 @@ body[data-theme='freedom'].body--dark {
color: #fff !important;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .q-drawer--dark,
body[data-theme=cyber].body--dark,
[data-theme=cyber] .q-menu--dark {
@@ -172,11 +240,23 @@ body[data-theme='cyber'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .q-card--dark,
[data-theme=cyber] .q-stepper--dark {
background: #1b1b1b !important;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
body[data-theme=cyber].body--light {
background-color: #f4f4f4;
}
[data-theme=cyber] .bg-primary {
background: #7cb342 !important;
}
@@ -214,6 +294,14 @@ body[data-theme='cyber'].body--dark {
color: #fff !important;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .q-drawer--dark,
body[data-theme=mint].body--dark,
[data-theme=mint] .q-menu--dark {
@@ -226,11 +314,23 @@ body[data-theme='mint'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .q-card--dark,
[data-theme=mint] .q-stepper--dark {
background: #334642 !important;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
body[data-theme=mint].body--light {
background-color: #f4f4f4;
}
[data-theme=mint] .bg-primary {
background: #3ab77d !important;
}
@@ -268,6 +368,14 @@ body[data-theme='mint'].body--dark {
color: #fff !important;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .q-drawer--dark,
body[data-theme=autumn].body--dark,
[data-theme=autumn] .q-menu--dark {
@@ -280,11 +388,23 @@ body[data-theme='autumn'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .q-card--dark,
[data-theme=autumn] .q-stepper--dark {
background: #463f33 !important;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
body[data-theme=autumn].body--light {
background-color: #f4f4f4;
}
[data-theme=autumn] .bg-primary {
background: #b7763a !important;
}
@@ -322,6 +442,14 @@ body[data-theme='autumn'].body--dark {
color: rgb(255, 255, 255) !important;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .q-drawer--dark,
body[data-theme=flamingo].body--dark,
[data-theme=flamingo] .q-menu--dark {
@@ -334,11 +462,23 @@ body[data-theme='flamingo'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .q-card--dark,
[data-theme=flamingo] .q-stepper--dark {
background: #bc23bc !important;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
body[data-theme=flamingo].body--light {
background-color: #f4f4f4;
}
[data-theme=flamingo] .bg-primary {
background: #ff00ff !important;
}
@@ -376,6 +516,14 @@ body[data-theme='flamingo'].body--dark {
color: rgb(255, 255, 255) !important;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .q-drawer--dark,
body[data-theme=monochrome].body--dark,
[data-theme=monochrome] .q-menu--dark {
@@ -388,11 +536,23 @@ body[data-theme='monochrome'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .q-card--dark,
[data-theme=monochrome] .q-stepper--dark {
background: rgb(39, 39, 39) !important;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
body[data-theme=monochrome].body--light {
background-color: #f4f4f4;
}
[data-theme=monochrome] .bg-primary {
background: #494949 !important;
}
@@ -482,6 +642,12 @@ body.body--dark .q-field--error .q-field__messages {
width: 500px;
}
.lnbits__table-bordered td,
.lnbits__table-bordered th {
border: 1px solid black;
border-collapse: collapse;
}
.q-table--dense th:first-child,
.q-table--dense td:first-child,
.q-table--dense .q-table__bottom {
@@ -535,12 +701,13 @@ video {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.qrcode__wrapper canvas {
width: 100% !important;
height: 100% !important;
max-width: 350px;
max-width: 320px;
}
.qrcode__image {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

+198 -16
View File
@@ -2,8 +2,10 @@ window.localisation.br = {
confirm: 'Sim',
server: 'Servidor',
theme: 'Tema',
site_customisation: 'Customização do Site',
funding: 'Financiamento',
users: 'Usuários',
audit: 'Auditoria',
apps: 'Aplicativos',
channels: 'Canais',
transactions: 'Transações',
@@ -26,9 +28,11 @@ window.localisation.br = {
restart: 'Reiniciar servidor',
save: 'Salvar',
save_tooltip: 'Salvar suas alterações',
topup: 'Recarregar',
topup_wallet: 'Recarregar uma carteira',
topup_hint: 'Use o ID da carteira para recarregar qualquer carteira',
credit_debit: 'Crédito / Débito',
credit_hint: 'Pressione Enter para creditar a conta',
credit_label: '{denomination} para creditar',
credit_ok:
'Sucesso ao creditar/debitar fundos virtuais ({amount} sats). Os pagamentos dependem dos fundos reais na fonte de financiamento.',
restart_tooltip: 'Reinicie o servidor para que as alterações tenham efeito',
add_funds_tooltip: 'Adicionar fundos a uma carteira.',
reset_defaults: 'Redefinir para padrões',
@@ -36,14 +40,13 @@ window.localisation.br = {
'Apagar todas as configurações e redefinir para os padrões.',
download_backup: 'Fazer backup do banco de dados',
name_your_wallet: 'Nomeie sua carteira {name}',
wallet_topup_ok:
'Sucesso ao criar fundos virtuais ({amount} sats). Pagamentos dependem dos fundos reais na fonte de financiamento.',
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 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.',
wallet: 'Carteira:',
wallets: 'Carteiras',
add_wallet: 'Adicionar nova carteira',
delete_wallet: 'Excluir carteira',
@@ -70,8 +73,6 @@ window.localisation.br = {
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',
@@ -88,7 +89,7 @@ window.localisation.br = {
pay: 'Pagar',
memo: 'Memo',
date: 'Data',
processing_payment: 'Processando pagamento...',
payment_processing: 'Processando pagamento...',
not_enough_funds: 'Fundos insuficientes!',
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
invoice_waiting: 'Fatura aguardando pagamento',
@@ -107,10 +108,15 @@ window.localisation.br = {
'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: 'Gerenciar',
exchanges: 'Bolsas de valores',
extensions: 'Extensões',
no_extensions: 'Você não possui nenhuma extensão instalada :(',
created: 'Criado',
search_extensions: 'Extensões de pesquisa',
extension_sources: 'Fontes de Extensão',
ext_sources_hint: 'Repositórios de onde as extensões podem ser baixadas',
ext_sources_label:
'URL de origem (use apenas a fonte oficial da extensão LNbits e fontes confiáveis)',
warning: 'Aviso',
repository: 'Repositório',
confirm_continue: 'Você tem certeza de que deseja continuar?',
@@ -119,8 +125,10 @@ window.localisation.br = {
uninstall: 'Desinstalar',
drop_db: 'Remover Dados',
enable: 'Ativar',
pay_to_enable: 'Pague para Habilitar',
enable_extension_details: 'Ativar extensão para o usuário atual',
disable: 'Desativar',
delete: 'Excluir',
installed: 'Instalado',
activated: 'Ativado',
deactivated: 'Desativado',
@@ -145,15 +153,19 @@ window.localisation.br = {
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
extension_required_lnbits_version:
'Esta versão requer no mínimo a versão do LNbits',
min_version: 'Mínimo (incluído)',
max_version: 'Máximo (excluído)',
payment_hash: 'Hash de pagamento',
fee: 'Taxa',
amount: 'Quantidade',
amount_sats: 'Quantidade (sats)',
tag: 'Etiqueta',
unit: 'Unidade',
description: 'Descrição',
expiry: 'Validade',
webhook: 'Webhook',
payment_proof: 'Comprovante de pagamento',
update: 'Atualizar',
update_available: 'Atualização {version} disponível!',
latest_update: 'Você está na versão mais recente {version}.',
notifications: 'Notificações',
@@ -163,12 +175,6 @@ window.localisation.br = {
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.',
@@ -185,7 +191,6 @@ window.localisation.br = {
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',
@@ -217,6 +222,7 @@ window.localisation.br = {
login_to_account: 'Faça login na sua conta',
create_account: 'Criar conta',
account_settings: 'Configurações da Conta',
signin_with_nostr: 'Continuar com Nostr',
signin_with_google: 'Entrar com o Google',
signin_with_github: 'Entrar com GitHub',
signin_with_keycloak: 'Entrar com Keycloak',
@@ -225,11 +231,14 @@ window.localisation.br = {
password_config: 'Configuração de Senha',
password_repeat: 'Repetição de senha',
change_password: 'Alterar Senha',
update_credentials: 'Atualizar credenciais',
update_pubkey: 'Atualizar Chave Pública',
set_password: 'Definir Senha',
invalid_password: 'A senha deve ter pelo menos 8 caracteres',
login: 'Entrar',
register: 'Registrar',
username: 'Nome de usuário',
pubkey: 'Chave Pública',
user_id: 'ID do Usuário',
email: 'E-mail',
first_name: 'Primeiro Nome',
@@ -244,14 +253,187 @@ window.localisation.br = {
back: 'Voltar',
logout: 'Sair',
look_and_feel: 'Aparência',
toggle_gradient: 'Alternar Gradiente',
gradient_background: 'Fundo em Degradê',
language: 'Idioma',
color_scheme: 'Esquema de Cores',
admin_settings: 'Configurações do Administrador',
extension_cost: 'Este lançamento requer um pagamento mínimo de {cost} sats.',
extension_paid_sats: 'Você já pagou {paid_sats} sats.',
release_details_error: 'Não é possível obter os detalhes da versão.',
pay_from_wallet: 'Pagar com a Carteira',
wallet_required: 'Carteira *',
show_qr: 'Exibir QR',
retry_install: 'Repetir Instalação',
new_payment: 'Efetuar Novo Pagamento',
hide_empty_wallets: 'Ocultar carteiras vazias'
update_payment: 'Atualizar Pagamento',
already_paid_question: 'Você já pagou?',
sell: 'Vender',
sell_require: 'Peça pagamento para habilitar a extensão',
sell_info:
'A extensão {name} requer um pagamento mínimo de {amount} sats para habilitar.',
hide_empty_wallets: 'Ocultar carteiras vazias',
recheck: 'Verificar novamente',
contributors: 'Contribuidores',
license: 'Licença',
reset_key: 'Redefinir Chave',
reset_password: 'Redefinir senha',
border_choices: 'Opções de Borda',
select_all: 'Selecionar tudo',
nfc_supported: 'Compatível com NFC',
nfc_not_supported: 'NFC não suportado',
expire_date: 'Data de Expiração:',
hash: 'Hash:',
welcome_lnbits: 'Bem-vindo ao LNbits',
setup_su_account: 'Configure a conta Superuser abaixo.',
create_ticker_converter: 'Criar Conversor de Ticker de Moeda',
enable_audit: 'Habilitar Auditoria',
recommended: 'Recomendado',
audit_desc: 'Gravar solicitações HTTP de acordo com os filtros especificados',
audit_record_req: 'Gravar Corpo da Requisição',
audit_record_warning: 'Aviso:',
audit_record_req_warning_1:
'dados confidenciais (como senhas) serão registrados.',
audit_record_req_warning_2:
'o corpo da solicitação pode ter um tamanho grande.',
audit_record_use: 'Use com cuidado.',
audit_ip: 'Registrar endereço IP',
audit_ip_desc: 'Registre o endereço IP do cliente',
audit_path_params: 'Registrar Parâmetros de Caminho',
audit_query_params: 'Registrar Parâmetros de Consulta',
audit_http_methods: 'Incluir métodos HTTP',
audit_http_methods_hint:
'Lista de métodos HTTP a serem incluídos. Listas vazias significam todos.',
audit_http_methods_label: 'Métodos HTTP',
audit_resp_codes: 'Incluir Códigos de Resposta HTTP',
audit_resp_codes_hint:
'Lista de códigos HTTP a serem incluídos (correspondência regex). Listas vazias significam todos. Ex: 4.*, 5.*',
audit_resp_codes_label: 'Código de resposta HTTP (regex)',
audit_paths: 'Incluir Caminhos',
audit_paths_hint:
'Lista de caminhos a serem incluídos (correspondência de regex). Lista vazia significa todos.',
audit_paths_label: 'Caminho HTTP (regex)',
audit_paths_exclude: 'Excluir Caminhos',
audit_paths_exclude_hint:
'Lista de caminhos a serem excluídos (correspondência regex). Lista vazia significa nenhum.',
audit_paths_exclude_label: 'Caminho HTTP (regex)',
exchange_providers: 'Provedores de Câmbio',
admin_extensions: 'Extensões de Administração',
admin_extensions_label: 'Extensões de administração',
admin_extensions_hint:
'Somente usuários com privilégios de administrador podem usar extensões.',
user_default_extensions: 'Extensões Padrão do Usuário',
user_default_extensions_label: 'Extensões do usuário',
user_default_extensions_hint:
'Extensões que serão ativadas por padrão para os usuários.',
miscellanous: 'Diversos',
misc_disable_extensions: 'Desativar extensões',
misc_disable_extensions_label: 'Desativar todas as extensões',
misc_hide_api: 'Ocultar API',
misc_hide_api_label:
'Oculta a API de carteira, extensões podem optar por honrar',
wallets_management: 'Gerenciamento de Carteiras',
funding_source_info: 'Informações da Fonte de Financiamento',
funding_source: 'Fonte de Financiamento: {wallet_class}',
node_balance: 'Saldo do Nó: {balance} sats',
lnbits_balance: 'Saldo do LNbits: {balance} sats',
funding_reserve_percent: 'Reserve Percentual: {percent} %',
node_managment: 'Gerenciamento de Nós',
node_management_not_supported:
'Gerenciamento de nó não suportado pela fonte de financiamento ativa',
toggle_node_ui: 'Interface do Nó',
toggle_public_node_ui: 'Interface Pública do Nó',
toggle_transactions_node_ui:
'Guia de Transações (Desativar em nós grandes CLN)',
invoice_expiry: 'Expiração da Fatura',
invoice_expiry_label: 'Validade da fatura (segundos)',
fee_reserve: 'Reserva de Taxa',
fee_reserve_msats: 'Taxa de reserva em msats',
fee_reserve_percent: 'Taxa de reserva em porcentagem',
server_management: 'Gerenciamento de Servidor',
base_url: 'URL base',
base_url_label: 'URL estática/base para o servidor',
authentication: 'Autenticação',
auth_token_expiry_label: 'Minutos para expiração do token',
auth_token_expiry_hint: 'Tempo em minutos até o token expirar',
auth_allowed_methods_label: 'Métodos de autorização permitidos',
auth_allowed_methods_hint: 'Selecione métodos de autorização',
auth_nostr_label: 'URL de Solicitação Nostr',
auth_nostr_hint: 'URL absoluta que os clientes usarão para fazer login.',
auth_google_ci_label: 'ID do Cliente do Google',
auth_google_ci_hint:
'Certifique-se de que os URIs de redirecionamento autorizados contenham https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Segredo do Cliente do Google',
auth_gh_client_id_label: 'ID do Cliente do GitHub',
auth_gh_client_id_hint:
'Certifique-se de que a URL de callback de autorização esteja definida como https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'Segredo do Cliente do GitHub',
auth_keycloak_label: 'URL de Descoberta do Keycloak',
auth_keycloak_ci_label: 'ID do Cliente Keycloak',
auth_keycloak_ci_hint:
'Certifique-se de que a URL de retorno de chamada de autorização esteja definida para https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Segredo do Cliente Keycloak',
currency_settings: 'Configurações de Moeda',
allowed_currencies: 'Moedas Permitidas',
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
default_account_currency: 'Moeda Padrão da Conta',
default_account_currency_hint: 'Moeda padrão para contabilidade',
service_fee_label: 'Taxa de serviço (%)',
service_fee_hint: 'Taxa cobrada por tx (%)',
service_fee_max_label: 'Taxa de serviço máx (sats)',
service_fee_max_hint: 'Taxa máxima de serviço a cobrar em (sats)',
fee_wallet: 'Carteira de Taxas',
fee_wallet_label: 'Carteira de tarifas (ID da carteira)',
fee_wallet_hint: 'ID da carteira para enviar fundos para',
disable_fee: 'Desativar Taxa',
disable_fee_internal: 'Desativar taxa de serviço para pagamentos internos',
disable_fee_internal_desc:
'Desativar Taxa de Serviço para Pagamentos Internos Lightning',
ui_management: 'Gerenciamento de UI',
ui_site_title: 'Título do Site',
ui_site_tagline: 'Tagline do site',
ui_elements_enable: 'Habilitar elementos na página inicial',
ui_elements_disable: 'Desativar elementos na página inicial',
ui_toggle_elements_tip:
"Remover elementos da página inicial, como 'funciona com', etc.",
ui_site_description: 'Descrição do Site',
ui_site_description_hint: 'Use texto simples, Markdown ou HTML bruto',
ui_default_wallet_name: 'Nome Padrão da Carteira',
lnbits_wallet: 'Carteira LNbits',
denomination: 'Denominação',
denomination_hint: 'O nome para o token FakeWallet',
ui_qr_code_logo: 'Logo do QR Code',
ui_qr_code_logo_hint: 'URL para imagem de logo no código QR',
ui_custom_badge: 'Distintivo Personalizado',
ui_custom_badge_label:
"Distintivo Personalizado 'USE COM CUIDADO - a carteira LNbits ainda está em BETA'",
ui_custom_badge_color_label: 'Cor Personalizada do Distintivo',
themes: 'Temas',
themes_hint: 'Escolha temas disponíveis para usuários',
custom_logo: 'Logotipo personalizado',
custom_logo_hint: 'URL para a imagem do logotipo',
ad_space_title: 'Título do Espaço Publicitário',
ad_space_title_label: 'Suportado por',
ad_slots: 'Slots de Anúncio',
ad_slots_hint:
'Adicionar URL e caminhos de arquivo de imagem no formato CSV, as extensões podem optar por honrar',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Anúncios Ativados',
ads_disabled: 'Anúncios Desativados',
user_management: 'Gerenciamento de Usuários',
admin_users: 'Usuários Administradores',
admin_users_hint: 'Usuários com privilégios de administrador',
admin_users_label: 'ID do Usuário',
allowed_users: 'Usuários Permitidos',
allowed_users_hint: 'Somente esses usuários podem usar o LNbits',
allowed_users_label: 'ID do Usuário',
allow_creation_user: 'Permitir a criação de novos usuários',
allow_creation_user_desc:
'Permitir a criação de novos usuários na página de índice',
components: 'Componentes',
long_running_endpoints: 'Top 5 Endpoints de Longa Execução',
http_request_methods: 'Métodos de Requisição HTTP',
http_response_codes: 'Códigos de Resposta HTTP',
request_details: 'Detalhes do Pedido',
http_request_details: 'Detalhes da Requisição HTTP'
}
+184 -16
View File
@@ -2,8 +2,10 @@ window.localisation.cn = {
confirm: '确定',
server: '服务器',
theme: '主题',
site_customisation: '网站定制',
funding: '资金',
users: '用户',
audit: '审计',
apps: '应用程序',
channels: '频道',
transactions: '交易记录',
@@ -26,23 +28,24 @@ window.localisation.cn = {
restart: '重新启动服务器',
save: '保存',
save_tooltip: '保存更改',
topup: '充值',
topup_wallet: '给钱包充值',
topup_hint: '使用钱包ID为任何钱包充值',
credit_debit: '信用卡 / 借记卡',
credit_hint: '按 Enter 键充值账户',
credit_label: '{denomination} 充值',
credit_ok:
'成功记入/扣除虚拟资金 ({amount} sats)。付款取决于资金来源的实际资金。',
restart_tooltip: '重新启动服务器以使更改生效',
add_funds_tooltip: '为钱包添加资金',
reset_defaults: '重置为默认设置',
reset_defaults_tooltip: '删除所有设置并重置为默认设置',
download_backup: '下载数据库备份',
name_your_wallet: '给你的 {name}钱包起个名字',
wallet_topup_ok:
'成功创建虚拟资金({amount} sats)。付款取决于资金来源的实际资金。',
paste_invoice_label: '粘贴发票,付款请求或lnurl*',
lnbits_description:
'LNbits 设置简单、轻量级,可以在任何闪电网络的资金来源上运行,甚至可以在LNbits自身上运行!您可以为自己运行LNbits,或者轻松为他人提供托管解决方案。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
export_to_phone: '通过二维码导出到手机',
export_to_phone_desc:
'这个二维码包含您钱包的URL。您可以使用手机扫描的方式打开您的钱包。',
wallet: '钱包:',
wallets: '钱包',
add_wallet: '添加新钱包',
delete_wallet: '删除钱包',
@@ -67,8 +70,6 @@ window.localisation.cn = {
api_keys_api_docs: '节点URL、API密钥和API文档',
lnbits_version: 'LNbits版本',
runs_on: '可运行在',
credit_hint: '按 Enter 键充值账户',
credit_label: '{denomination} 充值',
paste: '粘贴',
paste_from_clipboard: '从剪贴板粘贴',
paste_request: '粘贴请求',
@@ -85,7 +86,7 @@ window.localisation.cn = {
pay: '付款',
memo: '备注',
date: '日期',
processing_payment: '正在处理支付...',
payment_processing: '正在处理支付...',
not_enough_funds: '资金不足!',
search_by_tag_memo_amount: '按标签、备注、金额搜索',
invoice_waiting: '待支付的发票',
@@ -104,10 +105,15 @@ window.localisation.cn = {
'登录功能将在以后的更新中发布,请将此页面加为书签,以便将来访问您的钱包!此服务处于测试阶段,我们不对资金的丢失承担任何责任。',
no_transactions: '尚未进行任何交易',
manage: '管理',
exchanges: '交易所',
extensions: '扩展程序',
no_extensions: '你没有安装任何扩展程序 :(',
created: '已创建',
search_extensions: '搜索扩展程序',
extension_sources: '扩展源',
ext_sources_hint: '可以下载扩展的存储库',
ext_sources_label:
'来源网址(仅使用官方LNbits扩展程序来源和您可以信任的来源)',
warning: '警告',
repository: '代码库',
confirm_continue: '你确定要继续吗?',
@@ -116,8 +122,10 @@ window.localisation.cn = {
uninstall: '卸载',
drop_db: '删除数据',
enable: '启用',
pay_to_enable: '支付以启用',
enable_extension_details: '为当前用户启用扩展程序',
disable: '禁用',
delete: '删除',
installed: '已安装',
activated: '已激活',
deactivated: '已停用',
@@ -137,15 +145,19 @@ window.localisation.cn = {
extension_db_drop_warning:
'您即将删除该扩展的所有数据。请继续输入扩展程序名称以确认操作:',
extension_required_lnbits_version: '此版本要求最低的 LNbits 版本为',
min_version: '最小值(包含)',
max_version: '最大值(不含)',
payment_hash: '付款哈希',
fee: '费',
amount: '金额',
amount_sats: '金额(聪)',
tag: '标签',
unit: '单位',
description: '详情',
expiry: '过期时间',
webhook: 'Webhook',
payment_proof: '付款证明',
update: '更新',
update_available: '更新{version}可用!',
latest_update: '您当前使用的是最新版本{version}。',
notifications: '通知',
@@ -154,12 +166,7 @@ window.localisation.cn = {
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。更新后您将需要手动启用。',
@@ -175,7 +182,6 @@ window.localisation.cn = {
more: '更多',
less: '少',
releases: '版本',
killswitch: '杀手锏',
watchdog: '监控程序',
server_logs: '服务器日志',
ip_blocker: 'IP 阻止器',
@@ -206,6 +212,7 @@ window.localisation.cn = {
login_to_account: '登录您的账户',
create_account: '创建账户',
account_settings: '账户设置',
signin_with_nostr: '继续使用 Nostr',
signin_with_google: '使用谷歌账号登录',
signin_with_github: '使用GitHub登录',
signin_with_keycloak: '使用Keycloak登录',
@@ -214,11 +221,14 @@ window.localisation.cn = {
password_config: '密码配置',
password_repeat: '密码重复',
change_password: '修改密码',
update_credentials: '更新凭证',
update_pubkey: '更新公钥',
set_password: '设置密码',
invalid_password: '密码至少需要有8个字符',
login: '登录',
register: '注册',
username: '用户名',
pubkey: '公钥',
user_id: '用户ID',
email: '电子邮件',
first_name: '名字',
@@ -233,14 +243,172 @@ window.localisation.cn = {
back: '返回',
logout: '注销',
look_and_feel: '外观和感觉',
toggle_gradient: '切换渐变',
gradient_background: '渐变背景',
language: '语言',
color_scheme: '配色方案',
admin_settings: '管理员设置',
extension_cost: '此版本需要支付最低 {cost} sats。',
extension_paid_sats: '您已经支付了{paid_sats} sats。',
release_details_error: '无法获取发布详情。',
pay_from_wallet: '从钱包支付',
wallet_required: '钱包 *',
show_qr: '显示QR码',
retry_install: '重试安装',
new_payment: '创建新支付',
hide_empty_wallets: '隐藏空钱包'
update_payment: '更新付款',
already_paid_question: '你已经付款了吗?',
sell: '出售',
sell_require: '请求付款以启用扩展',
sell_info: '{name} 扩展需要支付至少 {amount} sat 才能启用。',
hide_empty_wallets: '隐藏空钱包',
recheck: '重新检查',
contributors: '贡献者们',
license: '许可证',
reset_key: '重置密钥',
reset_password: '重置密码',
border_choices: '边框选项',
select_all: '全选',
nfc_supported: '支持NFC',
nfc_not_supported: '不支持NFC',
expire_date: '有效期:',
hash: '哈希:',
welcome_lnbits: '欢迎来到LNbits',
setup_su_account: '设置超级用户账户如下。',
create_ticker_converter: '创建货币代码转换器',
enable_audit: '启用审核',
recommended: '推荐',
audit_desc: '根据指定的过滤器记录HTTP请求',
audit_record_req: '记录请求主体',
audit_record_warning: '警告:',
audit_record_req_warning_1: '机密数据(如密码)将被记录。',
audit_record_req_warning_2: '请求主体可能会有较大尺寸。',
audit_record_use: '请谨慎使用。',
audit_ip: '记录 IP 地址',
audit_ip_desc: '记录客户端的IP地址',
audit_path_params: '记录路径参数',
audit_query_params: '记录查询参数',
audit_http_methods: '包括 HTTP 方法',
audit_http_methods_hint: '要包含的 HTTP 方法列表。空列表表示全部。',
audit_http_methods_label: 'HTTP 方法',
audit_resp_codes: '包括 HTTP 响应代码',
audit_resp_codes_hint:
'要包含的 HTTP 代码列表(正则表达式匹配)。空列表表示全部。例如:4.*,5.*',
audit_resp_codes_label: 'HTTP响应代码(正则表达式)',
audit_paths: '包含路径',
audit_paths_hint: '要包含的路径列表(正则表达式匹配)。空列表意味着全部。',
audit_paths_label: 'HTTP 路径(正则表达式)',
audit_paths_exclude: '排除路径',
audit_paths_exclude_hint:
'要排除的路径列表(正则表达式匹配)。空列表表示没有。',
audit_paths_exclude_label: 'HTTP 路径(正则表达式)',
exchange_providers: '兑换提供商',
admin_extensions: '管理员扩展',
admin_extensions_label: '管理员扩展件',
admin_extensions_hint: '只有具有管理员权限的用户才能使用扩展程序',
user_default_extensions: '用户默认扩展',
user_default_extensions_label: '用户扩展',
user_default_extensions_hint: '对用户默认启用的扩展。',
miscellanous: '杂项',
misc_disable_extensions: '禁用扩展程序',
misc_disable_extensions_label: '禁用所有扩展程序',
misc_hide_api: '隐藏 API',
misc_hide_api_label: '隐藏钱包 api,扩展程序可以选择遵守',
wallets_management: '钱包管理',
funding_source_info: '资金来源信息',
funding_source: '资金来源:{wallet_class}',
node_balance: '节点余额:{balance} sats',
lnbits_balance: 'LNbits 余额:{balance} sats',
funding_reserve_percent: '保留百分比: {percent} %',
node_managment: '节点管理',
node_management_not_supported: '活动资金来源不支持节点管理',
toggle_node_ui: '节点用户界面',
toggle_public_node_ui: '公共节点用户界面',
toggle_transactions_node_ui: '交易选项卡(在大型 CLN 节点上禁用)',
invoice_expiry: '发票到期',
invoice_expiry_label: '发票到期(秒)',
fee_reserve: '费用储备',
fee_reserve_msats: '以msats计的保留费',
fee_reserve_percent: '以百分比计的保留费用',
server_management: '服务器管理',
base_url: '基本URL',
base_url_label: '服务器的静态/基本网址',
authentication: '认证',
auth_token_expiry_label: '令牌过期分钟数',
auth_token_expiry_hint: '令牌过期的剩余时间(分钟)',
auth_allowed_methods_label: '允许的授权方法',
auth_allowed_methods_hint: '选择授权方法',
auth_nostr_label: 'Nostr请求URL',
auth_nostr_hint: '客户端将用于登录的绝对URL。',
auth_google_ci_label: '谷歌客户ID',
auth_google_ci_hint:
'确保授权重定向URI包含https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google客户端密钥',
auth_gh_client_id_label: 'GitHub 客户端 ID',
auth_gh_client_id_hint:
'确保授权回调 URL 设置为 https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub客户端密码',
auth_keycloak_label: 'Keycloak 发现 URL',
auth_keycloak_ci_label: 'Keycloak 客户端 ID',
auth_keycloak_ci_hint:
'确保授权回调URL设置为https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak客户端密钥',
currency_settings: '货币设置',
allowed_currencies: '允许的货币',
allowed_currencies_hint: '限制可用法定货币的数量',
default_account_currency: '默认账户货币',
default_account_currency_hint: '默认的会计货币',
service_fee_label: '服务费 (%)',
service_fee_hint: '每笔交易收取的费用 (%)',
service_fee_max_label: '服务费最大值(聪)',
service_fee_max_hint: '最大服务费以 (sats) 收取',
fee_wallet: '费用钱包',
fee_wallet_label: '费用钱包(钱包 ID',
fee_wallet_hint: '用于接收资金的钱包 ID',
disable_fee: '禁用费用',
disable_fee_internal: '禁用内部付款服务费',
disable_fee_internal_desc: '禁用内部闪电支付的服务费',
ui_management: '用户界面管理',
ui_site_title: '网站标题',
ui_site_tagline: '网站标语',
ui_elements_enable: '在主页上启用元素',
ui_elements_disable: '禁用主页上的元素',
ui_toggle_elements_tip: '移除主页元素,例如“运行于”等。',
ui_site_description: '网站描述',
ui_site_description_hint: '使用纯文本、Markdown或原始HTML',
ui_default_wallet_name: '默认钱包名称',
lnbits_wallet: 'LNbits 钱包',
denomination: '面额',
denomination_hint: 'FakeWallet 代币的名称',
ui_qr_code_logo: '二维码标志',
ui_qr_code_logo_hint: '二维码中标志图像的 URL',
ui_custom_badge: '自定义徽章',
ui_custom_badge_label: '自定义徽章“慎用 - LNbits 钱包仍在测试阶段”',
ui_custom_badge_color_label: '自定义徽章颜色',
themes: '主题',
themes_hint: '选择可供用户使用的主题',
custom_logo: '自定义徽标',
custom_logo_hint: '徽标图像的URL',
ad_space_title: '广告位标题',
ad_space_title_label: '由...支持',
ad_slots: '广告位',
ad_slots_hint: '广告网址和图像文件路径以CSV格式存储,扩展可以选择遵循。',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: '启用广告',
ads_disabled: '广告已禁用',
user_management: '用户管理',
admin_users: '管理员用户',
admin_users_hint: '具有管理员权限的用户',
admin_users_label: '用户ID',
allowed_users: '允许的用户',
allowed_users_hint: '仅这些用户可以使用LNbits',
allowed_users_label: '用户 ID',
allow_creation_user: '允许创建新用户',
allow_creation_user_desc: '允许在索引页面上创建新用户',
components: '组件',
long_running_endpoints: '前五个长时间运行的端点',
http_request_methods: 'HTTP请求方法',
http_response_codes: 'HTTP响应代码',
request_details: '请求详情',
http_request_details: 'HTTP请求详细信息'
}
+196 -19
View File
@@ -2,8 +2,10 @@ window.localisation.cs = {
confirm: 'Ano',
server: 'Server',
theme: 'Téma',
site_customisation: 'Přizpůsobení stránek',
funding: 'Financování',
users: 'Uživatelé',
audit: 'Audit',
apps: 'Aplikace',
channels: 'Kanály',
transactions: 'Transakce',
@@ -26,23 +28,24 @@ window.localisation.cs = {
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',
credit_debit: 'Kreditní / Debetní',
credit_hint: 'Stiskněte Enter pro připsání na účet',
credit_label: '{denomination} k připsání',
credit_ok:
'Úspěšné připsání/odepsání virtuálních prostředků ({amount} satů). Platby závisí na skutečných prostředcích z financujícího zdroje.',
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',
wallet_topup_ok:
'Úspěšně vytvořeny virtuální prostředky ({amount} sats). Platby závisí na skutečných prostředcích na zdrojovém účtu.',
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 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.',
wallet: 'Peněženka:',
wallets: 'Peněženky',
add_wallet: 'Přidat novou peněženku',
delete_wallet: 'Smazat peněženku',
@@ -71,8 +74,6 @@ window.localisation.cs = {
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',
@@ -89,7 +90,7 @@ window.localisation.cs = {
pay: 'Platit',
memo: 'Poznámka',
date: 'Datum',
processing_payment: 'Zpracování platby...',
payment_processing: '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',
@@ -108,10 +109,15 @@ window.localisation.cs = {
'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',
exchanges: 'Burzy',
extensions: 'Rozšíření',
no_extensions: 'Nemáte nainstalováno žádné rozšíření :(',
created: 'Vytvořeno',
search_extensions: 'Hledat rozšíření',
extension_sources: 'Zdroje rozšíření',
ext_sources_hint: 'Úložiště, odkud lze rozšíření stáhnout.',
ext_sources_label:
'Zdrojová URL (používejte pouze oficiální zdroj rozšíření LNbits a zdroje, kterým můžete důvěřovat)',
warning: 'Varování',
repository: 'Repositář',
confirm_continue: 'Jste si jistí, že chcete pokračovat?',
@@ -120,8 +126,10 @@ window.localisation.cs = {
uninstall: 'Odinstalovat',
drop_db: 'Odstranit data',
enable: 'Povolit',
pay_to_enable: 'Zaplatit pro aktivaci',
enable_extension_details: 'Povolit rozšíření pro aktuálního uživatele',
disable: 'Zakázat',
delete: 'Smazat',
installed: 'Nainstalováno',
activated: 'Aktivováno',
deactivated: 'Deaktivováno',
@@ -145,15 +153,19 @@ window.localisation.cs = {
'Chystáte se odstranit všechna data pro rozšíření. Prosím, pokračujte zadáním názvu rozšíření:',
extension_required_lnbits_version:
'Toto vydání vyžaduje alespoň verzi LNbits',
min_version: 'Minimum (včetně)',
max_version: 'Maximální (vyloučeno)',
payment_hash: 'Hash platby',
fee: 'Poplatek',
amount: 'Částka',
amount_sats: 'Částka (sats)',
tag: 'Tag',
unit: 'Jednotka',
description: 'Popis',
expiry: 'Expirace',
webhook: 'Webhook',
payment_proof: 'Důkaz platby',
update: 'Aktualizovat',
update_available: 'Dostupná aktualizace {version}!',
latest_update: 'Máte nejnovější verzi {version}.',
notifications: 'Notifikace',
@@ -162,15 +174,6 @@ window.localisation.cs = {
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).',
@@ -184,7 +187,6 @@ window.localisation.cs = {
more: 'více',
less: 'méně',
releases: 'Vydání',
killswitch: 'Killswitch',
watchdog: 'Watchdog',
server_logs: 'Logy serveru',
ip_blocker: 'Blokování IP',
@@ -216,6 +218,7 @@ window.localisation.cs = {
login_to_account: 'Přihlaste se ke svému účtu',
create_account: 'Vytvořit účet',
account_settings: 'Nastavení účtu',
signin_with_nostr: 'Pokračovat s Nostr',
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',
@@ -224,11 +227,14 @@ window.localisation.cs = {
password_config: 'Konfigurace hesla',
password_repeat: 'Opakujte heslo',
change_password: 'Změnit heslo',
update_credentials: 'Aktualizovat přihlašovací údaje',
update_pubkey: 'Aktualizovat veřejný klíč',
set_password: 'Nastavit heslo',
invalid_password: 'Heslo musí mít alespoň 8 znaků',
login: 'Přihlášení',
register: 'Registrovat',
username: 'Uživatelské jméno',
pubkey: 'Veřejný klíč',
user_id: 'ID uživatele',
email: 'Email',
first_name: 'Křestní jméno',
@@ -243,14 +249,185 @@ window.localisation.cs = {
back: 'Zpět',
logout: 'Odhlásit se',
look_and_feel: 'Vzhled a chování',
toggle_gradient: 'Přepnout gradient',
gradient_background: 'Barevný přechod pozadí',
language: 'Jazyk',
color_scheme: 'Barevné schéma',
admin_settings: 'Nastavení administrátora',
extension_cost: 'Toto vydání vyžaduje minimální platbu {cost} satoshi.',
extension_paid_sats: 'Již jste zaplatili {paid_sats} sats.',
release_details_error: 'Nelze získat podrobnosti o vydání.',
pay_from_wallet: 'Platit z peněženky',
wallet_required: 'Peněženka *',
show_qr: 'Zobrazit QR',
retry_install: 'Zkusit znovu nainstalovat',
new_payment: 'Vytvořit novou platbu',
hide_empty_wallets: 'Skrýt prázdné peněženky'
update_payment: 'Aktualizovat platbu',
already_paid_question: 'Už jste zaplatili?',
sell: 'Prodat',
sell_require: 'Požádejte o platbu, abyste povolili rozšíření',
sell_info:
'Rozšíření {name} vyžaduje platbu minimálně {amount} sats pro aktivaci.',
hide_empty_wallets: 'Skrýt prázdné peněženky',
recheck: 'Znovu zkontrolovat',
contributors: 'Přispěvatelé',
license: 'Licence',
reset_key: 'Obnovit klíč',
reset_password: 'Obnovit heslo',
border_choices: 'Možnosti ohraničení',
select_all: 'Vybrat vše',
nfc_supported: 'Podpora NFC',
nfc_not_supported: 'NFC není podporováno',
expire_date: 'Datum expirace:',
hash: 'Hash:',
welcome_lnbits: 'Vítejte v LNbits',
setup_su_account: 'Nastavte účet Superuser níže.',
create_ticker_converter: 'Vytvořit převodník měnových tickerů',
enable_audit: 'Povolit audit',
recommended: 'Doporučeno',
audit_desc: 'Zaznamenávejte HTTP požadavky podle zadaných filtrů',
audit_record_req: 'Záznam Tělo Požadavku',
audit_record_warning: 'Varování:',
audit_record_req_warning_1: 'důvěrná data (jako hesla) budou zaznamenána.',
audit_record_req_warning_2: 'tělo žádosti může mít velkou velikost.',
audit_record_use: 'Používejte to opatrně.',
audit_ip: 'Zaznamenat IP adresu',
audit_ip_desc: 'Zaznamenejte IP adresu klienta',
audit_path_params: 'Zaznamenat parametry cesty',
audit_query_params: 'Zaznamenat parametry dotazu',
audit_http_methods: 'Zahrnout metody HTTP',
audit_http_methods_hint:
'Seznam metod HTTP, které mají být zahrnuty. Prázdné seznamy znamenají všechny.',
audit_http_methods_label: 'Metody HTTP',
audit_resp_codes: 'Zahrnout kódy odpovědí HTTP',
audit_resp_codes_hint:
'Seznam kódů HTTP, které mají být zahrnuty (regex match). Prázdné seznamy znamenají všechny. Např.: 4.*, 5.*',
audit_resp_codes_label: 'Kód odpovědi HTTP (regex)',
audit_paths: 'Zahrnout cesty',
audit_paths_hint:
'Seznam cest, které mají být zahrnuty (regex shoda). Prázdný seznam znamená vše.',
audit_paths_label: 'HTTP cesta (regex)',
audit_paths_exclude: 'Vyloučit cesty',
audit_paths_exclude_hint:
'Seznam cest, které mají být vyloučeny (regex shoda). Prázdný seznam znamená žádné.',
audit_paths_exclude_label: 'HTTP cesta (regex)',
exchange_providers: 'Poskytovatelé směny',
admin_extensions: 'Rozšíření pro správce',
admin_extensions_label: 'Administrátorské rozšíření',
admin_extensions_hint:
'Rozšíření může používat pouze uživatel s administrátorskými oprávněními.',
user_default_extensions: 'Výchozí rozšíření uživatele',
user_default_extensions_label: 'Uživatelská rozšíření',
user_default_extensions_hint:
'Rozšíření, která budou u uživatelů ve výchozím nastavení povolena.',
miscellanous: 'Různé',
misc_disable_extensions: 'Zakázat rozšíření',
misc_disable_extensions_label: 'Zakázat všechna rozšíření',
misc_hide_api: 'Skrýt API',
misc_hide_api_label:
'Skrývá API peněženky, rozšíření se mohou rozhodnout ctít',
wallets_management: 'Správa peněženek',
funding_source_info: 'Informace o zdroji financování',
funding_source: 'Zdroj financování: {wallet_class}',
node_balance: 'Stav uzlu: {balance} sats',
lnbits_balance: 'Zůstatek LNbits: {balance} sats',
funding_reserve_percent: 'Rezervovat procento: {percent} %',
node_managment: 'Správa uzlů',
node_management_not_supported:
'Správa uzlů není podporována aktivním zdrojem financování',
toggle_node_ui: 'Uživatelské rozhraní uzlu',
toggle_public_node_ui: 'Veřejné rozhraní uzlu',
toggle_transactions_node_ui:
'Karta Transakce (Zakázat na velkých uzlech CLN)',
invoice_expiry: 'Datum vypršení faktury',
invoice_expiry_label: 'Vypršení faktury (sekundy)',
fee_reserve: 'Rezerva poplatku',
fee_reserve_msats: 'Rezervační poplatek v msats',
fee_reserve_percent: 'Rezervační poplatek v procentech',
server_management: 'Správa serveru',
base_url: 'Základní URL',
base_url_label: 'Statická/Základní URL pro server',
authentication: 'Ověření',
auth_token_expiry_label: 'Minuty vypršení platnosti tokenu',
auth_token_expiry_hint: 'Čas v minutách do vypršení tokenu',
auth_allowed_methods_label: 'Povolené metody autorizace',
auth_allowed_methods_hint: 'Vyberte metody autorizace',
auth_nostr_label: 'URL žádosti Nostr',
auth_nostr_hint: 'Absolutní URL, které klienti použijí pro přihlášení.',
auth_google_ci_label: 'ID klienta Google',
auth_google_ci_hint:
'Ujistěte se, že autorizované přesměrovací URI obsahují https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Heslo klienta Google',
auth_gh_client_id_label: 'ID klienta GitHub',
auth_gh_client_id_hint:
'Ujistěte se, že je nastavena zpětná adresa URL pro autorizaci na https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub Klientský tajný klíč',
auth_keycloak_label: 'URL pro zjištění Keycloak',
auth_keycloak_ci_label: 'ID klienta Keycloak',
auth_keycloak_ci_hint:
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Klíč k aplikaci Keycloak tajemství',
currency_settings: 'Nastavení měny',
allowed_currencies: 'Povolené měny',
allowed_currencies_hint: 'Omezte počet dostupných fiat měn',
default_account_currency: 'Výchozí měna účtu',
default_account_currency_hint: 'Výchozí měna pro účetnictví',
service_fee_label: 'Poplatek za službu (%)',
service_fee_hint: 'Poplatek účtovaný za transakci (%)',
service_fee_max_label: 'Poplatek za službu max (sats)',
service_fee_max_hint: 'Maximální poplatek za službu k účtování v (sats)',
fee_wallet: 'Poplatková peněženka',
fee_wallet_label: 'Poplatková peněženka (ID peněženky)',
fee_wallet_hint: 'ID peněženky, na kterou se mají odeslat prostředky',
disable_fee: 'Zakázat poplatek',
disable_fee_internal: 'Zakázat poplatek za službu pro interní platby',
disable_fee_internal_desc:
'Zakázat servisní poplatek za interní lightning platby',
ui_management: 'Správa uživatelského rozhraní',
ui_site_title: 'Název stránky',
ui_site_tagline: 'Stránkový slogan',
ui_elements_enable: 'Povolit prvky na domovské stránce',
ui_elements_disable: 'Zakázat prvky na úvodní stránce',
ui_toggle_elements_tip:
"Odebrat prvky z domovské stránky, jako je 'běží na' atd.",
ui_site_description: 'Popis webu',
ui_site_description_hint: 'Použijte prostý text, Markdown nebo surové HTML.',
ui_default_wallet_name: 'Výchozí název peněženky',
lnbits_wallet: 'Peněženka LNbits',
denomination: 'Nominální hodnota',
denomination_hint: 'Název pro token FakeWallet',
ui_qr_code_logo: 'Logo QR kódu',
ui_qr_code_logo_hint: 'URL k obrázku loga v QR kódu',
ui_custom_badge: 'Vlastní odznak',
ui_custom_badge_label:
"Vlastní odznak 'POUŽÍVEJTE S OPATRNOSTÍ - Peněženka LNbits je stále v BETA verzi'",
ui_custom_badge_color_label: 'Barva vlastního odznaku',
themes: 'Motivy',
themes_hint: 'Vyberte motivy dostupné pro uživatele',
custom_logo: 'Vlastní logo',
custom_logo_hint: 'URL k obrázku loga',
ad_space_title: 'Název reklamního prostoru',
ad_space_title_label: 'Podporováno',
ad_slots: 'Reklamní sloty',
ad_slots_hint:
'Adresa URL reklamy a cesty k souborům obrázků ve formátu CSV, rozšíření se mohou rozhodnout respektovat',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Reklamy povoleny',
ads_disabled: 'Reklamy deaktivovány',
user_management: 'Správa uživatelů',
admin_users: 'Administrátorští uživatelé',
admin_users_hint: 'Uživatelé s administrátorskými oprávněními',
admin_users_label: 'ID uživatele',
allowed_users: 'Povolení uživatelé',
allowed_users_hint: 'Pouze tito uživatelé mohou používat LNbits.',
allowed_users_label: 'Uživatelské ID',
allow_creation_user: 'Povolit vytvoření nových uživatelů',
allow_creation_user_desc:
'Povolit vytváření nových uživatelů na úvodní stránce',
components: 'Soubory',
long_running_endpoints: 'Top 5 dlouho běžících koncových bodů',
http_request_methods: 'Metody HTTP požadavků',
http_response_codes: 'Kódy HTTP odpovědí',
request_details: 'Podrobnosti žádosti',
http_request_details: 'Podrobnosti HTTP žádosti'
}
+201 -16
View File
@@ -2,8 +2,10 @@ window.localisation.de = {
confirm: 'Ja',
server: 'Server',
theme: 'Theme',
site_customisation: 'Website-Anpassung',
funding: 'Funding',
users: 'Benutzer',
audit: 'Prüfung',
apps: 'Apps',
channels: 'Kanäle',
transactions: 'Transaktionen',
@@ -26,9 +28,11 @@ window.localisation.de = {
restart: 'Server neu starten',
save: 'Speichern',
save_tooltip: 'Änderungen speichern',
topup: 'Aufladen',
topup_wallet: 'Wallet aufladen',
topup_hint: 'Nutze die Wallet-ID, um eine beliebige Wallet aufzuladen',
credit_debit: 'Kredit / Debit',
credit_hint: 'Klicke Enter, um das Konto zu belasten',
credit_label: '{denomination} zu belasten',
credit_ok:
'Erfolgreiches Gutschreiben/Abziehen von virtuellen Geldern ({amount} Sats). Zahlungen hängen von den tatsächlichen Mitteln der Finanzierungsquelle ab.',
restart_tooltip: 'Starte den Server neu, um die Änderungen zu übernehmen',
add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.',
reset_defaults: 'Zurücksetzen',
@@ -36,8 +40,6 @@ window.localisation.de = {
'Alle Einstellungen auf die Standardeinstellungen zurücksetzen.',
download_backup: 'Datenbank-Backup herunterladen',
name_your_wallet: 'Vergib deiner {name} Wallet einen Namen',
wallet_topup_ok:
'Erfolg beim Erstellen von virtuellen Mitteln ({amount} Satoshis). Zahlungen hängen von den tatsächlichen Mitteln der Finanzierungsquelle ab.',
paste_invoice_label:
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
lnbits_description:
@@ -45,6 +47,7 @@ window.localisation.de = {
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.',
wallet: 'Brieftasche:',
wallets: 'Wallets',
add_wallet: 'Wallet hinzufügen',
delete_wallet: 'Wallet löschen',
@@ -71,8 +74,6 @@ window.localisation.de = {
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',
@@ -90,7 +91,7 @@ window.localisation.de = {
pay: 'Zahlen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Zahlung wird verarbeitet ...',
payment_processing: 'Zahlung wird verarbeitet ...',
not_enough_funds: 'Geldmittel sind erschöpft!',
search_by_tag_memo_amount: 'Suche nach Tag, Memo, Betrag',
invoice_waiting: 'Rechnung wartend auf Zahlung',
@@ -109,10 +110,16 @@ window.localisation.de = {
'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: 'Verwalten',
exchanges: 'Börsenplätze',
extensions: 'Erweiterungen',
no_extensions: 'Du hast noch keine Erweiterungen installiert :(',
created: 'Erstellt',
search_extensions: 'Sucherweiterungen',
extension_sources: 'Erweiterungsquellen',
ext_sources_hint:
'Repositorys, von denen die Erweiterungen heruntergeladen werden können.',
ext_sources_label:
'Quell-URL (verwenden Sie nur die offizielle LNbits-Erweiterungsquelle und vertrauenswürdige Quellen)',
warning: 'Warnung',
repository: 'Repository',
confirm_continue: 'Bist du sicher, dass du fortfahren möchtest?',
@@ -121,8 +128,10 @@ window.localisation.de = {
uninstall: 'Deinstallieren',
drop_db: 'Daten löschen',
enable: 'Aktivieren',
pay_to_enable: 'Zahlen Sie zum Aktivieren',
enable_extension_details: 'Erweiterung für aktuellen Benutzer aktivieren',
disable: 'Deaktivieren',
delete: 'Löschen',
installed: 'Installiert',
activated: 'Aktiviert',
deactivated: 'Deaktiviert',
@@ -147,15 +156,19 @@ window.localisation.de = {
'Sie sind dabei, alle Daten für die Erweiterung zu entfernen. Bitte geben Sie den Namen der Erweiterung ein, um fortzufahren:',
extension_required_lnbits_version:
'Diese Version erfordert mindestens die LNbits-Version',
min_version: 'Mindestwert (inklusive)',
max_version: 'Maximalwert (ausgeschlossen)',
payment_hash: 'Zahlungs-Hash',
fee: 'Gebühr',
amount: 'Menge',
amount_sats: 'Betrag (sats)',
tag: 'Tag',
unit: 'Einheit',
description: 'Beschreibung',
expiry: 'Ablauf',
webhook: 'Webhook',
payment_proof: 'Beleg',
update: 'Aktualisieren',
update_available: 'Aktualisierung {version} verfügbar!',
latest_update: 'Sie sind auf der neuesten Version {version}.',
notifications: 'Benachrichtigungen',
@@ -164,12 +177,6 @@ window.localisation.de = {
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.',
@@ -186,7 +193,7 @@ window.localisation.de = {
more: 'mehr',
less: 'weniger',
releases: 'Veröffentlichungen',
killswitch: 'Killswitch',
watchdog: 'Wachhund',
server_logs: 'Serverprotokolle',
ip_blocker: 'IP-Sperre',
@@ -220,6 +227,7 @@ window.localisation.de = {
login_to_account: 'Melden Sie sich bei Ihrem Konto an',
create_account: 'Konto erstellen',
account_settings: 'Kontoeinstellungen',
signin_with_nostr: 'Mit Nostr fortfahren',
signin_with_google: 'Mit Google anmelden',
signin_with_github: 'Anmelden mit GitHub',
signin_with_keycloak: 'Mit Keycloak anmelden',
@@ -228,11 +236,14 @@ window.localisation.de = {
password_config: 'Passwortkonfiguration',
password_repeat: 'Passwortwiederholung',
change_password: 'Passwort ändern',
update_credentials: 'Anmeldeinformationen aktualisieren',
update_pubkey: 'Öffentlichen Schlüssel aktualisieren',
set_password: 'Passwort festlegen',
invalid_password: 'Das Passwort muss mindestens 8 Zeichen haben.',
login: 'Anmelden',
register: 'Registrieren',
username: 'Benutzername',
pubkey: 'Öffentlicher Schlüssel',
user_id: 'Benutzer-ID',
email: 'E-Mail',
first_name: 'Vorname',
@@ -247,15 +258,189 @@ window.localisation.de = {
back: 'Zurück',
logout: 'Abmelden',
look_and_feel: 'Aussehen und Verhalten',
toggle_gradient: 'Verlauf umschalten',
gradient_background: 'Verlaufs-Hintergrund',
language: 'Sprache',
color_scheme: 'Farbschema',
admin_settings: 'Admin-Einstellungen',
extension_cost:
'Diese Version erfordert eine Zahlung von mindestens {cost} Sats.',
extension_paid_sats: 'Sie haben bereits {paid_sats} Sats bezahlt.',
release_details_error: 'Kann die Details zur Veröffentlichung nicht abrufen.',
pay_from_wallet: 'Zahlen aus dem Geldbeutel',
wallet_required: 'Wallet *',
show_qr: 'QR anzeigen',
retry_install: 'Installieren erneut versuchen',
new_payment: 'Neue Zahlung vornehmen',
hide_empty_wallets: 'Leere Geldbörsen verbergen'
update_payment: 'Zahlung aktualisieren',
already_paid_question: 'Haben Sie schon bezahlt?',
sell: 'Verkaufen',
sell_require: 'Zahlung anfordern, um die Erweiterung zu aktivieren',
sell_info:
'Die {name}-Erweiterung erfordert eine Zahlung von mindestens {amount} Satoshis, um aktiviert zu werden.',
hide_empty_wallets: 'Leere Geldbörsen verbergen',
recheck: 'Erneut überprüfen',
contributors: 'Mitwirkende',
license: 'Lizenz',
reset_key: 'Zurücksetzen-Schlüssel',
reset_password: 'Passwort zurücksetzen',
border_choices: 'Randoptionen',
select_all: 'Alles auswählen',
nfc_supported: 'NFC unterstützt',
nfc_not_supported: 'NFC wird nicht unterstützt',
expire_date: 'Ablaufdatum:',
hash: 'Hash:',
welcome_lnbits: 'Willkommen bei LNbits',
setup_su_account: 'Richten Sie das Superuser-Konto unten ein.',
create_ticker_converter: 'Währungsticker-Konverter erstellen',
enable_audit: 'Audit aktivieren',
recommended: 'Empfohlen',
audit_desc: 'HTTP-Anfragen entsprechend den angegebenen Filtern aufzeichnen',
audit_record_req: 'Anfragekörper aufzeichnen',
audit_record_warning: 'Warnung:',
audit_record_req_warning_1:
'Vertrauliche Daten (wie Passwörter) werden protokolliert.',
audit_record_req_warning_2: 'Der Anfragetext kann groß sein.',
audit_record_use: 'Verwenden Sie es mit Vorsicht.',
audit_ip: 'IP-Adresse aufzeichnen',
audit_ip_desc: 'Speichern Sie die IP-Adresse des Clients',
audit_path_params: 'Pfadparameter aufzeichnen',
audit_query_params: 'Abfrageparameter aufzeichnen',
audit_http_methods: 'HTTP-Methoden einschließen',
audit_http_methods_hint:
'Liste der HTTP-Methoden, die einbezogen werden sollen. Leere Listen bedeuten alle.',
audit_http_methods_label: 'HTTP-Methoden',
audit_resp_codes: 'HTTP-Antwortcodes einbeziehen',
audit_resp_codes_hint:
'Liste der einzuschließenden HTTP-Codes (regex-Match). Leere Liste bedeutet alle. Z.B.: 4.*, 5.*',
audit_resp_codes_label: 'HTTP-Antwortcode (Regex)',
audit_paths: 'Einfügepfade',
audit_paths_hint:
'Liste der aufzunehmenden Pfade (Regex-Übereinstimmung). Leere Liste bedeutet alle.',
audit_paths_label: 'HTTP-Pfad (Regex)',
audit_paths_exclude: 'Pfade ausschließen',
audit_paths_exclude_hint:
'Liste der auszuschließenden Pfade (regex-Match). Leere Liste bedeutet keine.',
audit_paths_exclude_label: 'HTTP-Pfad (Regex)',
exchange_providers: 'Austauschdienste',
admin_extensions: 'Admin-Erweiterungen',
admin_extensions_label: 'Admin-Erweiterungen',
admin_extensions_hint:
'Nur Benutzer mit Admin-Rechten können Erweiterungen verwenden.',
user_default_extensions: 'Standarderweiterungen des Benutzers',
user_default_extensions_label: 'Benutzererweiterungen',
user_default_extensions_hint:
'Erweiterungen, die standardmäßig für die Benutzer aktiviert werden.',
miscellanous: 'Verschiedenes',
misc_disable_extensions: 'Erweiterungen deaktivieren',
misc_disable_extensions_label: 'Alle Erweiterungen deaktivieren',
misc_hide_api: 'API ausblenden',
misc_hide_api_label: 'Verbirgt Wallet-API, Erweiterungen können es ehren',
wallets_management: 'Verwaltung von Geldbörsen',
funding_source_info: 'Finanzierungsquelleninformationen',
funding_source: 'Finanzierungsquelle: {wallet_class}',
node_balance: 'Kontostand: {balance} Sats',
lnbits_balance: 'LNbits-Guthaben: {balance} Sats',
funding_reserve_percent: 'Reservieren Prozent: {percent} %',
node_managment: 'Knotenverwaltung',
node_management_not_supported:
'Knotenverwaltung wird von der aktiven Finanzierungsquelle nicht unterstützt',
toggle_node_ui: 'Node-Benutzeroberfläche',
toggle_public_node_ui: 'Öffentliche Knoten-Benutzeroberfläche',
toggle_transactions_node_ui:
'Transaktionen-Tab (Bei großen CLN-Knoten deaktivieren)',
invoice_expiry: 'Rechnungsablauf',
invoice_expiry_label: 'Rechnungsablauf (Sekunden)',
fee_reserve: 'Gebührenreserve',
fee_reserve_msats: 'Reservierungsgebühr in msats',
fee_reserve_percent: 'Reservierungsgebühr in Prozent',
server_management: 'Serververwaltung',
base_url: 'Basis-URL',
base_url_label: 'Statische/Basis-URL für den Server',
authentication: 'Authentifizierung',
auth_token_expiry_label: 'Token-Ablaufminuten',
auth_token_expiry_hint: 'Zeit in Minuten bis der Token abläuft',
auth_allowed_methods_label: 'Erlaubte Autorisierungsmethoden',
auth_allowed_methods_hint: 'Wählen Sie Autorisierungsmethoden aus',
auth_nostr_label: 'Nostr-Anforderungs-URL',
auth_nostr_hint: 'Absolute URL, die die Clients für die Anmeldung verwenden.',
auth_google_ci_label: 'Google-Client-ID',
auth_google_ci_hint:
'Stellen Sie sicher, dass die autorisierten Umleitungs-URIs https://{domain}/api/v1/auth/google/token enthalten',
auth_google_cs_label: 'Google-Client-Geheimnis',
auth_gh_client_id_label: 'GitHub-Client-ID',
auth_gh_client_id_hint:
'Stellen Sie sicher, dass die URL für den Autorisierungsrückruf auf https://{domain}/api/v1/auth/github/token gesetzt ist.',
auth_gh_client_secret_label: 'GitHub-Client-Geheimnis',
auth_keycloak_label: 'Keycloak Discovery-URL',
auth_keycloak_ci_label: 'Keycloak-Client-ID',
auth_keycloak_ci_hint:
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/keycloak/token eingestellt ist.',
auth_keycloak_cs_label: 'Keycloak-Client-Geheimnis',
currency_settings: 'Währungseinstellungen',
allowed_currencies: 'Erlaubte Währungen',
allowed_currencies_hint:
'Begrenzen Sie die Anzahl der verfügbaren Fiat-Währungen',
default_account_currency: 'Standardkontowährung',
default_account_currency_hint: 'Standardwährung für Buchhaltung',
service_fee_label: 'Servicegebühr (%)',
service_fee_hint: 'Gebühr pro Transaktion (%)',
service_fee_max_label: 'Servicegebühr max. (sats)',
service_fee_max_hint: 'Maximale Servicegebühr in (sats) berechnen.',
fee_wallet: 'Gebühren-Wallet',
fee_wallet_label: 'Gebühren-Wallet (Wallet-ID)',
fee_wallet_hint: 'Wallet-ID, an die Gelder gesendet werden sollen',
disable_fee: 'Gebühr deaktivieren',
disable_fee_internal:
'Dienstleistungsgebühr für interne Zahlungen deaktivieren',
disable_fee_internal_desc:
'Dienstleistungsgebühr für interne Lightning-Zahlungen deaktivieren',
ui_management: 'UI-Verwaltung',
ui_site_title: 'Seitentitel',
ui_site_tagline: 'Seitenslogan',
ui_elements_enable: 'Elemente auf der Startseite aktivieren',
ui_elements_disable: 'Elemente auf der Startseite deaktivieren',
ui_toggle_elements_tip:
"Entfernen Sie Homepage-Elemente wie 'läuft auf' usw.",
ui_site_description: 'Seitenbeschreibung',
ui_site_description_hint:
'Verwenden Sie einfachen Text, Markdown oder rohes HTML',
ui_default_wallet_name: 'Standard-Walletname',
lnbits_wallet: 'LNbits-Wallet',
denomination: 'Nomination',
denomination_hint: 'Der Name für das FakeWallet-Token',
ui_qr_code_logo: 'QR-Code-Logo',
ui_qr_code_logo_hint: 'URL zum Logo-Bild im QR-Code',
ui_custom_badge: 'Benutzerdefiniertes Abzeichen',
ui_custom_badge_label:
"Benutzerdefiniertes Abzeichen 'MIT VORSICHT VERWENDEN - LNbits-Wallet ist noch in der BETA-Phase'",
ui_custom_badge_color_label: 'Benutzerdefinierte Abzeichenfarbe',
themes: 'Themen',
themes_hint: 'Wählen Sie Themen, die für Benutzer verfügbar sind',
custom_logo: 'Benutzerdefiniertes Logo',
custom_logo_hint: 'URL zum Logobild',
ad_space_title: 'Anzeigentitel',
ad_space_title_label: 'Unterstützt von',
ad_slots: 'Werbeplätze',
ad_slots_hint:
'URL-Adressen und Bilddateipfade im CSV-Format, Erweiterungen können darauf achten',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Anzeigen aktiviert',
ads_disabled: 'Anzeigen deaktiviert',
user_management: 'Benutzerverwaltung',
admin_users: 'Admin-Benutzer',
admin_users_hint: 'Benutzer mit Administratorrechten',
admin_users_label: 'Benutzer-ID',
allowed_users: 'Zugelassene Benutzer',
allowed_users_hint: 'Nur diese Benutzer können LNbits verwenden.',
allowed_users_label: 'Benutzer-ID',
allow_creation_user: 'Erlauben Sie die Erstellung neuer Benutzer',
allow_creation_user_desc:
'Erlauben Sie das Erstellen neuer Benutzer auf der Indexseite',
components: 'Komponenten',
long_running_endpoints: 'Top 5 lang laufende Endpunkte',
http_request_methods: 'HTTP-Anfragemethoden',
http_response_codes: 'HTTP-Antwortcodes',
request_details: 'Anfragedetails',
http_request_details: 'HTTP-Anfragedetails'
}
+316 -20
View File
@@ -6,6 +6,7 @@ window.localisation.en = {
funding: 'Funding',
users: 'Users',
audit: 'Audit',
api_watch: 'Api Watch',
apps: 'Apps',
channels: 'Channels',
transactions: 'Transactions',
@@ -26,6 +27,7 @@ window.localisation.en = {
close_channel: 'Close Channel',
close: 'Close',
restart: 'Restart server',
image_library: 'Image Library',
save: 'Save',
save_tooltip: 'Save your changes',
credit_debit: 'Credit / Debit',
@@ -45,6 +47,8 @@ window.localisation.en = {
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.',
access_wallet_on_mobile: 'Mobile Access',
wallet: 'Wallet: ',
wallets: 'Wallets',
add_wallet: 'Add a new wallet',
delete_wallet: 'Delete wallet',
@@ -56,12 +60,14 @@ window.localisation.en = {
currency: 'Currency',
update_currency: 'Update currency',
press_to_claim: 'Press to claim bitcoin',
claim_desc:
'It seems you have a claimable amount of bitcoin but you dont have a wallet yet. Press the button below to claim it. This will create a new wallet for you.',
donate: 'Donate',
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_badge: 'Service fee: {amount} % per transaction',
service_fee_max_badge:
'Service fee: {amount} % per transaction (max {max} {denom})',
service_fee_tooltip:
'Service fee charged by the LNbits server admin per outgoing transaction',
toggle_darkmode: 'Toggle Dark Mode',
@@ -77,6 +83,7 @@ window.localisation.en = {
create_invoice: 'Create Invoice',
camera_tooltip: 'Use camera to scan an invoice/QR',
export_csv: 'Export to CSV',
export_csv_details: 'Export to CSV with details',
chart_tooltip: 'Show chart',
pending: 'Pending',
copy_invoice: 'Copy invoice',
@@ -84,15 +91,22 @@ window.localisation.en = {
cancel: 'Cancel',
scan: 'Scan',
read: 'Read',
write: 'Write',
pay: 'Pay',
memo: 'Memo',
date: 'Date',
processing_payment: 'Processing payment...',
path: 'Path',
payment_processing: 'Processing payment...',
payment_processing: 'Processing payment...',
payment_successful: 'Payment successful!',
payment_pending: 'Payment pending...',
payment_check: 'Check payment',
not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount',
invoice_waiting: 'Invoice waiting to be paid',
payment_received: 'Payment Received',
payment_sent: 'Payment Sent',
payment_failed: 'Payment Failed',
receive: 'receive',
send: 'send',
outgoing_payment_pending: 'Outgoing payment pending',
@@ -103,7 +117,7 @@ window.localisation.en = {
copy_wallet_url: 'Copy wallet URL',
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.\n\nFind your login credentials on your account settings page.\n\nThis service is in BETA. LNbits holds no responsibility for loss of access to funds.',
'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.\n\nFind your login credentials on your account settings page.\n\nLNbits holds no responsibility for loss of access to funds.',
no_transactions: 'No transactions made yet',
manage: 'Manage',
exchanges: 'Exchanges',
@@ -111,6 +125,10 @@ window.localisation.en = {
no_extensions: "You don't have any extensions installed :(",
created: 'Created',
search_extensions: 'Search extensions',
extension_sources: 'Extension Sources',
ext_sources_hint: 'Repositories from where the extensions can be downloaded',
ext_sources_label:
'Source URL (only use the official LNbits extension source, and sources you can trust)',
warning: 'Warning',
repository: 'Repository',
confirm_continue: 'Are you sure you want to continue?',
@@ -158,20 +176,81 @@ window.localisation.en = {
payment_proof: 'Payment Proof',
update: 'Update',
update_available: 'Update {version} available!',
funding_sources: 'Funding Sources',
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
no_notifications: 'No notifications',
notifications_disabled: 'LNbits status notifications are disabled.',
enable_notifications: 'Enable Notifications',
enable_notifications_desc:
'If enabled it will fetch the latest LNbits Status updates, like security incidents and updates.',
enable_killswitch: 'Enable Killswitch',
enable_killswitch_desc:
'If enabled it will change your funding source to VoidWallet automatically if LNbits sends out a killswitch signal. You will need to enable manually after an update.',
killswitch_interval: 'Killswitch Interval',
killswitch_interval_desc:
'How often the background task should check for the LNbits killswitch signal from the status source (in minutes).',
enable_watchdog: 'Enable Watchdog',
notifications_configure: 'Configure Notifications',
notifications_nostr_config: 'Nostr Configuration',
notifications_enable_nostr: 'Enable Nostr',
notifications_enable_nostr_desc: 'Send notfications over Nostr',
notifications_nostr_private_key: 'Nostr Private Key',
notifications_nostr_private_key_desc:
'Private key (hex or nsec) to sign the messages sent to Nostr',
notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to',
notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram',
notifications_enable_telegram_desc: 'Send notfications over Telegram',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Access token for the bot',
notifications_chat_id: 'Chat ID',
notifications_chat_id_desc: 'Chat ID to send the notifications to',
notifications_email_config: 'Email Configuration',
notifications_enable_email: 'Enable Email',
notifications_enable_email_desc: 'Send notifications over email',
notifications_send_test_email: 'Send test email',
notifications_send_email: 'Send email',
notifications_send_email_desc: 'Email you will send from',
notifications_send_email_username: 'Username',
notifications_send_email_username_desc:
'Username, will use the email if not set',
notifications_send_email_password: 'Send email password',
notifications_send_email_password_desc:
'Password for the email you will send from',
notifications_send_email_server_port: 'Send email SMTP port',
notifications_send_email_server_port_desc: 'Port for the SMTP server',
notifications_send_email_server: 'Send email SMTP server',
notifications_send_email_server_desc:
'SMTP server for the email you will send from',
notifications_send_to_emails: 'Emails to send to',
notifications_send_to_emails_desc: 'Emails notifications will be sent to',
notification_settings_update: 'Settings updated',
notification_settings_update_desc:
'Notify when server settings have been updated',
notification_server_start_stop: 'Server Start/Stop',
notification_server_start_stop_desc:
'Notify when the server has been started/stopped',
notification_watchdog_limit: 'Watchdog Limit Notification',
notification_watchdog_limit_desc:
'Notify when the watchdog limit has been reached (does not affect the funding source)',
notification_server_status: 'Server Status',
notification_server_status_desc:
'Send regular notifications about the server status (interval value in hours)',
notification_incoming_payment: 'Incoming Payments',
notification_incoming_payment_desc:
'Notify when a wallet has received a payment above the specified amount (sats)',
notification_outgoing_payment: 'Outgoing Payments',
notification_outgoing_payment_desc:
'Notify when a wallet has sent a payment above the specified amount (sats)',
notification_credit_debit: 'Credit / Debit',
notification_credit_debit_desc:
'Notify when a wallet has been credited/debited by the superuser',
notification_balance_delta_changed: 'Balance Delta Changed',
notification_balance_delta_changed_desc:
'Notify when the diference between the node balance and the LNbits balance has changed even by 1 sat. This runs every minute.',
enable_watchdog: 'Enable Watchdog Switch',
enable_watchdog_desc:
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
watchdog_interval: 'Watchdog Interval',
@@ -187,7 +266,6 @@ window.localisation.en = {
more: 'more',
less: 'less',
releases: 'Releases',
killswitch: 'Killswitch',
watchdog: 'Watchdog',
server_logs: 'Server Logs',
ip_blocker: 'IP Blocker',
@@ -197,28 +275,39 @@ 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',
callback_url_rules: 'Callback URL Rules',
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
callback_url_rule_hint:
'Callback URLs (like LNURL one) will be validated against all of these rules. No rule means all URLs are allowed.',
wallet_limiter: 'Wallet Limiter',
wallet_config: 'Wallet Config',
wallet_charts: 'Wallet Charts',
wallet_limit_max_withdraw_per_day:
'Max daily wallet withdrawal in sats (0 for no limit, -1 to block withdrawal)',
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)',
only_incoming_payments_allowed: 'Only incoming payments allowed',
disable_outgoing_payments: 'Disable outgoing payments',
number_of_requests: 'Number of requests',
time_unit: 'Time unit',
minute: 'minute',
settings: 'Settings',
second: 'second',
hour: 'hour',
disable_server_log: 'Disable Server Log',
enable_server_log: 'Enable Server Log',
coming_soon: 'Feature coming soon',
session_has_expired: 'Your session has expired. Please login again.',
instant_access_question: 'Want instant access?',
instant_access_question: 'or 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_oauth: 'Login with',
signin_with_oauth_or: 'or Login with',
signin_with_nostr: 'Continue with Nostr',
signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub',
@@ -227,11 +316,13 @@ window.localisation.en = {
password: 'Password',
password_config: 'Password Config',
password_repeat: 'Password repeat',
update_password: 'Update Password',
change_password: 'Change Password',
update_credentials: 'Update Credentials',
update_pubkey: 'Update Public Key',
set_password: 'Set Password',
invalid_password: 'Password must have at least 8 characters',
invalid_password_repeat: 'Passwords do not match',
login: 'Login',
register: 'Register',
username: 'Username',
@@ -247,13 +338,23 @@ window.localisation.en = {
invalid_username: 'Invalid Username',
auth_provider: 'Auth Provider',
my_account: 'My Account',
existing_account_question: 'Already have an account?',
background_image: 'Background Image',
back: 'Back',
logout: 'Logout',
look_and_feel: 'Look and Feel',
api_token: 'API Token',
api_tokens: 'API Tokens',
access_control_list: 'Access Control List',
access_control_list_admin_warning:
'This is an admin account. The generated tokens will have admin privileges.',
new_api_acl: 'New Access Control List',
api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background',
language: 'Language',
color_scheme: 'Color Scheme',
admin_settings: 'Admin Settings',
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.',
@@ -275,5 +376,200 @@ window.localisation.en = {
reset_key: 'Reset Key',
reset_password: 'Reset Password',
border_choices: 'Border Choices',
select_all: 'Select All'
select_all: 'Select All',
nfc_supported: 'NFC Supported',
nfc_not_supported: 'NFC not Supported',
expire_date: 'Expire Date: ',
hash: 'Hash: ',
welcome_lnbits: 'Welcome to LNbits',
setup_su_account: 'Set up the Superuser account below.',
create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit',
recommended: 'Recommended',
audit_desc: 'Record HTTP requests according with the specified filters',
audit_record_req: 'Record Request Body',
audit_record_warning: 'Warning: ',
audit_record_req_warning_1:
'confidential data (like passwords) will be logged.',
audit_record_req_warning_2: 'the request body can have large size.',
audit_record_use: 'Use it with caution.',
audit_ip: 'Record IP Address',
audit_ip_desc: 'Record the IP address of the client',
audit_path_params: 'Record Path Parameters',
audit_query_params: 'Record Query Parameters',
audit_http_methods: 'Include HTTP Methods',
audit_http_methods_hint:
'List of HTTP methods to be included. Empty lists means all.',
audit_http_methods_label: 'HTTP Methods',
audit_resp_codes: 'Include HTTP Response Codes',
audit_resp_codes_hint:
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
audit_resp_codes_label: 'HTTP Response code (regex)',
audit_paths: 'Include Paths',
audit_paths_hint:
'List of paths to be included (regex match). Empty list means all.',
audit_paths_label: 'HTTP Path (regex)',
audit_paths_exclude: 'Exclude Paths',
audit_paths_exclude_hint:
'List of paths to be excluded (regex match). Empty list means none.',
audit_paths_exclude_label: 'HTTP Path (regex)',
exchange_providers: 'Exchange Providers',
admin_extensions: 'Admin Extensions',
admin_extensions_label: 'Admin extensions',
admin_extensions_hint: 'Extensions only user with admin privileges can use',
user_default_extensions: 'User Default Extensions',
user_default_extensions_label: 'User extensions',
user_default_extensions_hint:
'Extensions that will be enabled by default for the users.',
miscellanous: 'Miscellanous',
misc_disable_extensions: 'Disable Extensions',
misc_disable_extensions_label: 'Disable all extensions',
misc_hide_api: 'Hide API',
misc_hide_api_label: 'Hides wallet api, extensions can choose to honor',
wallets_management: 'Wallets Management',
funding_source_info: 'Funding Source Info',
funding_source: 'Funding Source: {wallet_class}',
node_balance: 'Node Balance: {balance} sats',
lnbits_balance: 'LNbits Balance: {balance} sats',
funding_reserve_percent: 'Reserve Percent: {percent} %',
node_managment: 'Node Management',
node_management_not_supported:
'Node Management not supported by active funding source',
toggle_node_ui: 'Node UI',
toggle_public_node_ui: 'Public Node UI',
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
invoice_expiry: 'Invoice Expiry',
invoice_expiry_label: 'Invoice expiry (seconds)',
fee_reserve: 'Fee Reserve',
fee_reserve_percent: 'Fee Reserve Percent',
fee_reserve_msats: 'Reserve fee in msats',
reserve_fee_in_percent: 'Reserve fee in percent',
payment_wait_time: 'Payment Wait Time',
payment_wait_time_desc:
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
server_management: 'Server Management',
base_url: 'Base URL',
base_url_label: 'Static/Base url for the server',
authentication: 'Authentication',
auth_token_expiry_label: 'Token expire minutes',
auth_token_expiry_hint: 'Time in minutes until the token expires',
auth_allowed_methods_label: 'Allowed authorization methods',
auth_allowed_methods_hint: 'Select authorization methods',
auth_nostr_label: 'Nostr Request URL',
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
auth_google_ci_label: 'Google Client ID',
auth_google_ci_hint:
'Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google Client Secret',
auth_gh_client_id_label: 'GitHub Client ID',
auth_gh_client_id_hint:
'Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub Client Secret',
auth_keycloak_label: 'Keycloak Discovery URL',
auth_keycloak_ci_label: 'Keycloak Client ID',
auth_keycloak_ci_hint:
'Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak Client Secret',
currency_settings: 'Currency Settings',
allowed_currencies: 'Allowed Currencies',
allowed_currencies_hint: 'Limit the number of available fiat currencies',
default_account_currency: 'Default Account Currency',
default_account_currency_hint: 'Default currency for accounting',
max_incoming_payment_amount: 'Max Incoming Payment Amount',
max_incoming_payment_amount_desc:
'Maximum amount allowed for generating an invoice',
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
max_outgoing_payment_amount_desc:
'Maximum amount allowed for making a payment',
service_fee: 'Service Fee',
service_fee_label: 'Service fee (%)',
service_fee_hint: 'Fee charged per tx (%)',
service_fee_max: 'Service Fee Max',
service_fee_max_label: 'Service fee max (sats)',
service_fee_max_hint: 'Max service fee to charge in (sats)',
fee_wallet: 'Fee Wallet',
fee_wallet_label: 'Fee wallet (wallet ID)',
fee_wallet_hint: 'Wallet ID to send funds to',
disable_fee: 'Disable Fee',
disable_fee_internal: 'Disable Service Fee for Internal Payments',
disable_fee_internal_desc:
'Disable Service Fee for Internal Lightning Payments',
ui_management: 'UI Management',
ui_site_title: 'Site Title',
ui_changing_remove_lnbits_elements:
' (changing will remove LNbits elements on the homepage and footer)',
ui_site_tagline: 'Site Tagline',
ui_elements_enable: 'Enable elements on homepage/footer',
ui_elements_disable: 'Disable elements on homepage/footer',
ui_toggle_elements_tip: "Remove homepage elements like 'runs on' etc",
ui_site_description: 'Site Description',
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
ui_default_wallet_name: 'Default Wallet Name',
ui_default_theme: 'Default Theme',
lnbits_wallet: 'LNbits wallet',
denomination: 'Denomination',
denomination_hint: 'The name for the FakeWallet token',
denomination_error: 'Denomination must be 3 characters, or `sats`',
ui_qr_code_logo: 'QR Code/Favicon Logo',
ui_qr_code_logo_hint: 'QR code and favicon logo url',
ui_custom_image: 'Custom Image',
ui_custom_image_label: 'URL to custom image',
ui_custom_image_hint: 'Image showed at homepage/login',
ui_custom_badge: 'Custom Badge',
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
ui_custom_badge_color_label: 'Custom Badge Color',
themes: 'Themes',
themes_hint: 'Choose themes available for users',
custom_logo: 'Custom Logo',
custom_logo_hint: 'URL to logo image',
ad_space_title: 'Ad Space Title',
ad_space_title_label: 'Supported by',
ad_slots: 'Advertisement Slots',
ad_slots_hint:
'Ad url and image filepaths in CSV format, extensions can choose to honor',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Ads Enabled',
ads_disabled: 'Ads Disabled',
user_management: 'User Management',
admin_users: 'Admin Users',
admin_users_hint: 'Users with admin privileges',
admin_users_label: 'User ID',
allowed_users: 'Allowed Users',
allowed_users_hint: 'Only these users can use LNbits',
allowed_users_label: 'User ID',
allow_creation_user: 'Allow creation of new users',
allow_creation_user_desc: 'Allow creation of new users on the index page',
new_user_not_allowed: 'Registration is disabled.',
components: 'Components',
long_running_endpoints: 'Top 5 Long Running Endpoints',
http_request_methods: 'HTTP Request Methods',
http_response_codes: 'HTTP Response Codes',
request_details: 'Request Details',
http_request_details: 'HTTP Request Details',
payment_details: 'Payment Details',
payment_details_desc: 'Detailed information about the payment',
payments: 'Payments',
payment_show_internal: 'Show Internal Payments',
payment_chart_flow: 'Monthly Payment Flow',
payment_chart_status: 'Payment Status',
payment_chart_tx_per_wallet: 'Transactions per Wallet (balance/count)',
payment_details_back: 'Back to Payments',
payment_chart_tags: 'Payments by Tags',
payments_balance_in_out: 'Balance In/Out',
payments_count_in_out: 'Count In/Out',
payments_status_chart: 'Status Chart',
payments_tag_chart: 'Tag Chart',
payments_balance_chart: 'Balance Chart',
payments_wallets_chart: 'Wallets Chart',
payments_balance_in_out_chart: 'Balance In/Out Chart',
payments_count_in_out_chart: 'Count In/Out Chart',
reset_wallet_keys: 'Reset Keys',
reset_wallet_keys_desc:
'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.',
view_list: 'View wallets as list',
view_column: 'View wallets as rows',
filter_payments: 'Filter payments',
filter_date: 'Filter by date',
websocket_example: 'Websocket example'
}
+204 -17
View File
@@ -2,8 +2,10 @@ window.localisation.es = {
confirm: 'Sí',
server: 'Servidor',
theme: 'Tema',
site_customisation: 'Personalización del sitio',
funding: 'Financiación',
users: 'Usuarios',
audit: 'Auditoría',
apps: 'Aplicaciones',
channels: 'Canales',
transactions: 'Transacciones',
@@ -26,9 +28,11 @@ window.localisation.es = {
restart: 'Reiniciar el servidor',
save: 'Guardar',
save_tooltip: 'Guardar cambios',
topup: 'Recargar',
topup_wallet: 'Recargar billetera',
topup_hint: 'Utilice el ID de billetera para recargar cualquier billetera',
credit_debit: 'Crédito / Débito',
credit_hint: 'Presione Enter para cargar la cuenta',
credit_label: 'Cargar {denomination}',
credit_ok:
'Éxito al acreditar/debitar fondos virtuales ({amount} sats). Los pagos dependen de los fondos reales en la fuente de financiación.',
restart_tooltip: 'Reinicie el servidor para aplicar los cambios',
add_funds_tooltip: 'Agregue fondos a una billetera.',
reset_defaults: 'Restablecer',
@@ -36,14 +40,13 @@ window.localisation.es = {
'Borrar todas las configuraciones y restablecer a los valores predeterminados.',
download_backup: 'Descargar copia de seguridad de la base de datos',
name_your_wallet: 'Nombre de su billetera {name}',
wallet_topup_ok:
'Éxito creando fondos virtuales ({amount} sats). Los pagos dependen de los fondos reales en la fuente de financiación.',
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 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í.',
wallet: 'Billetera:',
wallets: 'Billeteras',
add_wallet: 'Agregar nueva billetera',
delete_wallet: 'Eliminar billetera',
@@ -71,8 +74,6 @@ window.localisation.es = {
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',
@@ -89,7 +90,7 @@ window.localisation.es = {
pay: 'Pagar',
memo: 'Memo',
date: 'Fecha',
processing_payment: 'Procesando pago ...',
payment_processing: 'Procesando pago ...',
not_enough_funds: '¡No hay suficientes fondos!',
search_by_tag_memo_amount: 'Buscar por etiqueta, memo, cantidad',
invoice_waiting: 'Factura esperando pago',
@@ -108,10 +109,16 @@ window.localisation.es = {
'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: 'Administrar',
exchanges: 'Intercambios',
extensions: 'Extensiones',
no_extensions: 'No tienes extensiones instaladas :(',
created: 'Creado',
search_extensions: 'Extensiones de búsqueda',
extension_sources: 'Fuentes de extensión',
ext_sources_hint:
'Repositorios desde donde se pueden descargar las extensiones',
ext_sources_label:
'URL de origen (utilice solo la fuente oficial de la extensión LNbits y fuentes en las que pueda confiar)',
warning: 'Advertencia',
repository: 'Repositorio',
confirm_continue: '¿Está seguro de que desea continuar?',
@@ -120,8 +127,10 @@ window.localisation.es = {
uninstall: 'Desinstalar',
drop_db: 'Eliminar datos',
enable: 'Habilitar',
pay_to_enable: 'Pagar para habilitar',
enable_extension_details: 'Habilitar extensión para el usuario actual',
disable: 'Deshabilitar',
delete: 'Eliminar',
installed: 'Instalado',
activated: 'Activado',
deactivated: 'Desactivado',
@@ -146,15 +155,19 @@ window.localisation.es = {
'Está a punto de eliminar todos los datos para la extensión. Por favor, escriba el nombre de la extensión para continuar:',
extension_required_lnbits_version:
'Esta versión requiere al menos una versión de LNbits',
min_version: 'Mínimo (incluido)',
max_version: 'Máximo (excluido)',
payment_hash: 'Hash de pago',
fee: 'Cuota',
amount: 'Cantidad',
amount_sats: 'Cantidad (sats)',
tag: 'Etiqueta',
unit: 'Unidad',
description: 'Descripción',
expiry: 'Expiración',
webhook: 'Webhook',
payment_proof: 'Prueba de pago',
update: 'Actualizar',
update_available: '¡Actualización {version} disponible!',
latest_update: 'Usted está en la última versión {version}.',
notifications: 'Notificaciones',
@@ -164,13 +177,7 @@ window.localisation.es = {
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',
@@ -186,7 +193,6 @@ window.localisation.es = {
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',
@@ -220,6 +226,7 @@ window.localisation.es = {
login_to_account: 'Inicie sesión en su cuenta',
create_account: 'Crear cuenta',
account_settings: 'Configuración de la cuenta',
signin_with_nostr: 'Continuar con Nostr',
signin_with_google: 'Inicia sesión con Google',
signin_with_github: 'Inicia sesión con GitHub',
signin_with_keycloak: 'Iniciar sesión con Keycloak',
@@ -228,11 +235,14 @@ window.localisation.es = {
password_config: 'Configuración de Contraseña',
password_repeat: 'Repetición de contraseña',
change_password: 'Cambiar contraseña',
update_credentials: 'Actualizar credenciales',
update_pubkey: 'Actualizar clave pública',
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',
pubkey: 'Clave pública',
user_id: 'Identificación de usuario',
email: 'Correo electrónico',
first_name: 'Nombre de pila',
@@ -247,14 +257,191 @@ window.localisation.es = {
back: 'Atrás',
logout: 'Cerrar sesión',
look_and_feel: 'Apariencia',
toggle_gradient: 'Alternar degradado',
gradient_background: 'Fondo de gradiente',
language: 'Idioma',
color_scheme: 'Esquema de colores',
admin_settings: 'Configuración del administrador',
extension_cost: 'Esta versión requiere un pago mínimo de {cost} sats.',
extension_paid_sats: 'Ya has pagado {paid_sats} sats.',
release_details_error: 'No se pueden obtener los detalles de la versión.',
pay_from_wallet: 'Pagar desde la billetera',
wallet_required: 'Billetera *',
show_qr: 'Mostrar QR',
retry_install: 'Reintentar Instalación',
new_payment: 'Realizar nuevo pago',
hide_empty_wallets: 'Ocultar billeteras vacías'
update_payment: 'Actualizar Pago',
already_paid_question: '¿Ya has pagado?',
sell: 'Vender',
sell_require: 'Solicitar pago para habilitar la extensión',
sell_info:
'La extensión {name} requiere un pago mínimo de {amount} sats para habilitar.',
hide_empty_wallets: 'Ocultar billeteras vacías',
recheck: 'Revisar de nuevo',
contributors: 'Colaboradores',
license: 'Licencia',
reset_key: 'Restablecer clave',
reset_password: 'Restablecer contraseña',
border_choices: 'Opciones de Borde',
select_all: 'Seleccionar todo',
nfc_supported: 'Compatible con NFC',
nfc_not_supported: 'NFC no compatible',
expire_date: 'Fecha de vencimiento:',
hash: 'Hash:',
welcome_lnbits: 'Bienvenido a LNbits',
setup_su_account: 'Configura la cuenta de Superusuario a continuación.',
create_ticker_converter: 'Crear Convertidor de Ticker de Moneda',
enable_audit: 'Habilitar auditoría',
recommended: 'Recomendado',
audit_desc:
'Registrar solicitudes HTTP de acuerdo con los filtros especificados',
audit_record_req: 'Registrar cuerpo de solicitud',
audit_record_warning: 'Advertencia:',
audit_record_req_warning_1:
'los datos confidenciales (como las contraseñas) serán registrados.',
audit_record_req_warning_2:
'el cuerpo de la solicitud puede tener un tamaño grande.',
audit_record_use: 'Úsalo con precaución.',
audit_ip: 'Registrar Dirección IP',
audit_ip_desc: 'Registra la dirección IP del cliente',
audit_path_params: 'Registrar parámetros de ruta',
audit_query_params: 'Registrar parámetros de consulta',
audit_http_methods: 'Incluye métodos HTTP',
audit_http_methods_hint:
'Lista de métodos HTTP a incluir. Las listas vacías significan todos.',
audit_http_methods_label: 'Métodos HTTP',
audit_resp_codes: 'Incluir Códigos de Respuesta HTTP',
audit_resp_codes_hint:
'Lista de códigos HTTP a incluir (coincidencia regex). Listas vacías significan todos. Ej: 4.*, 5.*',
audit_resp_codes_label: 'Código de respuesta HTTP (regex)',
audit_paths: 'Incluir rutas',
audit_paths_hint:
'Lista de rutas a incluir (coincidencia de expresión regular). Lista vacía significa todas.',
audit_paths_label: 'Ruta HTTP (regex)',
audit_paths_exclude: 'Excluir rutas',
audit_paths_exclude_hint:
'Lista de rutas a excluir (coincidencia de expresiones regulares). Lista vacía significa ninguna.',
audit_paths_exclude_label: 'Ruta HTTP (regex)',
exchange_providers: 'Proveedores de intercambio',
admin_extensions: 'Extensiones de Administración',
admin_extensions_label: 'Extensiones de administración',
admin_extensions_hint:
'Solo los usuarios con privilegios de administrador pueden usar extensiones.',
user_default_extensions: 'Extensiones predeterminadas del usuario',
user_default_extensions_label: 'Extensiones de usuario',
user_default_extensions_hint:
'Extensiones que estarán habilitadas de forma predeterminada para los usuarios.',
miscellanous: 'Misceláneo',
misc_disable_extensions: 'Desactivar extensiones',
misc_disable_extensions_label: 'Desactivar todas las extensiones',
misc_hide_api: 'Ocultar API',
misc_hide_api_label:
'Oculta la API de la billetera, las extensiones pueden optar por respetar',
wallets_management: 'Gestión de Carteras',
funding_source_info: 'Información sobre la Fuente de Financiamiento',
funding_source: 'Fuente de financiamiento: {wallet_class}',
node_balance: 'Balance de Nodo: {balance} sats',
lnbits_balance: 'Saldo de LNbits: {balance} sats',
funding_reserve_percent: 'Reserve Porcentaje: {percent} %',
node_managment: 'Gestión de nodos',
node_management_not_supported:
'La gestión de nodos no es compatible con la fuente de financiación activa',
toggle_node_ui: 'Interfaz de usuario de nodo',
toggle_public_node_ui: 'Interfaz Pública de Nodo',
toggle_transactions_node_ui:
'Pestaña de transacciones (desactivar en nodos CLN grandes)',
invoice_expiry: 'Vencimiento de la Factura',
invoice_expiry_label: 'Expiración de la factura (segundos)',
fee_reserve: 'Reserva de tarifa',
fee_reserve_msats: 'Cuota de reserva en msats',
fee_reserve_percent: 'Tasa de reserva en porcentaje',
server_management: 'Gestión del Servidor',
base_url: 'URL base',
base_url_label: 'URL base estática para el servidor',
authentication: 'Autenticación',
auth_token_expiry_label: 'Minutos de vencimiento del token',
auth_token_expiry_hint: 'Tiempo en minutos hasta que el token expire',
auth_allowed_methods_label: 'Métodos de autorización permitidos',
auth_allowed_methods_hint: 'Seleccione métodos de autorización',
auth_nostr_label: 'URL de solicitud Nostr',
auth_nostr_hint:
'URL absoluto que los clientes utilizarán para iniciar sesión.',
auth_google_ci_label: 'ID de cliente de Google',
auth_google_ci_hint:
'Asegúrate de que los URIs de redirección autorizados contengan https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Secreto del Cliente de Google',
auth_gh_client_id_label: 'ID de cliente de GitHub',
auth_gh_client_id_hint:
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'Secreto del cliente de GitHub',
auth_keycloak_label: 'URL de descubrimiento de Keycloak',
auth_keycloak_ci_label: 'ID de cliente de Keycloak',
auth_keycloak_ci_hint:
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Secreto del Cliente de Keycloak',
currency_settings: 'Configuración de moneda',
allowed_currencies: 'Monedas permitidas',
allowed_currencies_hint:
'Limite el número de monedas fiduciarias disponibles',
default_account_currency: 'Moneda predeterminada de la cuenta',
default_account_currency_hint: 'Moneda predeterminada para contabilidad',
service_fee_label: 'Tarifa de servicio (%)',
service_fee_hint: 'Tarifa cobrada por tx (%)',
service_fee_max_label: 'Tarifa de servicio máx (sats)',
service_fee_max_hint: 'Tarifa máxima por servicio a cobrar en (sats)',
fee_wallet: 'Billetera de Tarifas',
fee_wallet_label: 'Billetera de tarifas (ID de billetera)',
fee_wallet_hint: 'ID de la billetera a la que enviar fondos',
disable_fee: 'Desactivar tarifa',
disable_fee_internal: 'Desactivar tarifa de servicio para pagos internos',
disable_fee_internal_desc:
'Desactivar tarifa de servicio para pagos internos Lightning',
ui_management: 'Gestión de la interfaz de usuario',
ui_site_title: 'Título del Sitio',
ui_site_tagline: 'Lema del sitio',
ui_elements_enable: 'Habilitar elementos en la página de inicio',
ui_elements_disable: 'Desactivar elementos en la página de inicio',
ui_toggle_elements_tip:
"Eliminar elementos de la página de inicio como 'funciona en', etc.",
ui_site_description: 'Descripción del sitio',
ui_site_description_hint:
'Usa texto sin formato, Markdown o HTML sin procesar',
ui_default_wallet_name: 'Nombre predeterminado de la billetera',
lnbits_wallet: 'Cartera LNbits',
denomination: 'Denominación',
denomination_hint: 'El nombre para el token FakeWallet',
ui_qr_code_logo: 'Logo de código QR',
ui_qr_code_logo_hint: 'URL a la imagen del logo en el código QR',
ui_custom_badge: 'Insignia personalizada',
ui_custom_badge_label:
"Insignia personalizada 'USAR CON PRECAUCIÓN - La billetera LNbits aún está en BETA'",
ui_custom_badge_color_label: 'Color personalizado de insignia',
themes: 'Temas',
themes_hint: 'Elige los temas disponibles para los usuarios',
custom_logo: 'Logotipo personalizado',
custom_logo_hint: 'URL a la imagen del logo',
ad_space_title: 'Título del Espacio Publicitario',
ad_space_title_label: 'Respaldado por',
ad_slots: 'Espacios publicitarios',
ad_slots_hint:
'URL de anuncio y rutas de archivo de imagen en formato CSV, las extensiones pueden optar por respetar',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Anuncios habilitados',
ads_disabled: 'Anuncios desactivados',
user_management: 'Gestión de Usuarios',
admin_users: 'Usuarios Administradores',
admin_users_hint: 'Usuarios con privilegios de administrador',
admin_users_label: 'ID de usuario',
allowed_users: 'Usuarios Permitidos',
allowed_users_hint: 'Solo estos usuarios pueden usar LNbits',
allowed_users_label: 'ID de usuario',
allow_creation_user: 'Permitir la creación de nuevos usuarios',
allow_creation_user_desc:
'Permitir la creación de nuevos usuarios en la página de índice',
components: 'Componentes',
long_running_endpoints: 'Principales 5 puntos de conexión de larga duración',
http_request_methods: 'Métodos de solicitud HTTP',
http_response_codes: 'Códigos de Respuesta HTTP',
request_details: 'Detalles de la solicitud',
http_request_details: 'Detalles de la Solicitud HTTP'
}
+370 -49
View File
@@ -2,8 +2,11 @@ window.localisation.fi = {
confirm: 'Kyllä',
server: 'Palvelin',
theme: 'Teema',
site_customisation: 'Sivuston kustomointi',
funding: 'Rahoitus',
users: 'Käyttäjät',
audit: 'Seuranta',
api_watch: 'API-seuranta',
apps: 'Sovellukset',
channels: 'Kanavat',
transactions: 'Tapahtumat',
@@ -24,27 +27,30 @@ window.localisation.fi = {
close_channel: 'Sulje kanava',
close: 'Sulje',
restart: 'Palvelimen uudelleen käynnistys',
image_library: 'Kuvakirjasto',
save: 'Tallenna',
save_tooltip: 'Tallenna muutokset',
topup: 'Topup',
topup_wallet: 'Lisää varoja lompakkoon',
topup_hint: 'Lisää varoja lompakkoon sen ID:n perusteella',
credit_debit: 'Hyvitä / Veloita',
credit_hint:
'Hyväksy painamalla Enter (negatiivisetkin arvot ovat sallittuja)',
credit_label: 'Hyvitä / Veloita tilille {denomination}-varoja',
credit_ok:
'Virtuaalivarojen ({amount} sat) hyvitys-/veloitustapahtuma onnistui. Maksukyky riippuuu rahoituslähteen todellisista varoista.',
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
add_funds_tooltip: 'Lisää varoja lompakkoon',
reset_defaults: 'Peruuta muutokset',
reset_defaults: 'Palauta oletusasetukset',
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',
wallet_topup_ok:
'Virtuaalisten varojen luominen onnistui ({amount} sats). Maksut riippuvat rahoituslähteen todellisista varoista.',
paste_invoice_label:
'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *',
name_your_wallet: 'Nimeä lompakkosi {name}',
paste_invoice_label: 'Liita lasku, maksupyyntö tai LNURL-koodi*',
lnbits_description:
'Kevyt ja helppokäyttöinen LNbits voi käyttää rahoituslähteinään erilaisia palveluita, ja jopa LNbits 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!',
'Kevyt ja helppokäyttöinen LNbits voi käyttää rahoituslähteinään mitä vain Lightning-palveluita ja jopa LNbits-palvelua! 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. ',
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voit lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
access_wallet_on_mobile: 'Mobiili käyttö',
wallet: 'Lompakko:',
wallets: 'Lompakot',
add_wallet: 'Lisää lompakko',
delete_wallet: 'Poista lompakko',
@@ -56,14 +62,13 @@ window.localisation.fi = {
currency: 'Valuutta',
update_currency: 'Tallenna',
press_to_claim: 'Lunasta varat painamalla tästä',
claim_desc:
'Näyttää että sinulla on lunastamattomia bitcoin varoja, mutta sinulla ei vielä ole lompakkoa. Lunasta varat allaolevaa nappia painamalla, ja sinulle luodaan lompakko.',
donate: 'Lahjoita',
view_github: 'Näytä GitHub:ssa',
voidwallet_active:
'Maksutapahtumat ovat poissa käytöstä, koska VoidWallet on aktiivinen!',
voidwallet_active: 'VoidWallet on aktiivinen. Se ei tue maksutapahtumia!',
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ä',
@@ -73,14 +78,13 @@ window.localisation.fi = {
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',
export_csv_details: 'Vie CSV-tiedostoon lisätietoineen',
chart_tooltip: 'Näytä kaaviokuva',
pending: 'Odottaa',
copy_invoice: 'Kopioi lasku',
@@ -88,15 +92,18 @@ window.localisation.fi = {
cancel: 'Peruuta',
scan: 'Scannaa',
read: 'Lue',
write: 'Kirjoita',
pay: 'Maksa',
memo: 'Kuvaus',
date: 'Päiväys',
processing_payment: 'Maksua käsitellään...',
path: 'Path',
payment_processing: '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',
payment_failed: 'Maksu epäonnistui',
receive: 'vastaanota',
send: 'lähetä',
outgoing_payment_pending: 'Lähtevä maksu odottaa',
@@ -107,13 +114,18 @@ window.localisation.fi = {
copy_wallet_url: 'Kopioi lompakon URL',
disclaimer_dialog_title: 'Tärkeää!',
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.',
'Sinun *PITÄÄ TALLETTAA* kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossa kirjautumaan lompakkoosi! Löydät kirjautumistiedot Tilin asetukset -sivulta. 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',
exchanges: 'Vaihtokurssit',
extensions: 'Laajennukset',
no_extensions: 'Laajennuksia ei ole asennettu :(',
created: 'Luotu',
search_extensions: 'Etsi laajennuksia',
extension_sources: 'Laajennuslähteet',
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
ext_sources_label:
'Lähde-URL (käytä vain virallista LNbits tai muuta luotettaa laajennuslähdettä)',
warning: 'Varoitus',
repository: 'Laajennuksien lähde',
confirm_continue: 'Haluatko varmasti jatkaa?',
@@ -122,8 +134,10 @@ window.localisation.fi = {
uninstall: 'Poista',
drop_db: 'Poista tiedot',
enable: 'Ota käyttöön',
pay_to_enable: 'Maksa ottaaksesi käyttöön',
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
disable: 'Poista käytöstä',
delete: 'Poista',
installed: 'Asennettu',
activated: 'Käytössä',
deactivated: 'Poissa käytöstä',
@@ -145,32 +159,96 @@ window.localisation.fi = {
extension_db_drop_warning:
'Olet tuhoamassa laajennuksen tallettamat tiedot. Vahvista poisto kirjoittamalla viivalle seuraavassa näkyvä laajennuksen nimi:',
extension_required_lnbits_version:
'Tämä julkaisu vaatii vähintään LNbits-version',
'Tämä laajennus vaatii vähintään LNbits-version',
min_version: 'Minimi (sisältyy)',
max_version: 'Enimmäismäärä (ei sisälly)',
payment_hash: 'Maksun tiiviste',
fee: 'Kulu',
amount: 'Määrä',
amount_sats: 'Määrä (sat)',
tag: 'Tunniste',
unit: 'Yksikkö',
description: 'Kuvaus',
expiry: 'Vanheneminen',
expiry: 'Vanhenee',
webhook: 'Webhook',
payment_proof: 'Maksun varmenne',
update_available: 'Saatavilla on päivitys versioon {version}!',
update: 'Päivitä',
update_available: 'Saatavilla on päivitys {version}-versioon!',
update_available: 'Rahoituslähteet',
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',
notifications_configure: 'Määritä tiedotukset',
notifications_nostr_config: 'Nostr-määritykset',
notifications_enable_nostr: 'Kaytä Nostr:ia',
notifications_enable_nostr_desc: 'Lähetä tietodukset Nostr:in kautta',
notifications_nostr_private_key: 'Nostr-yksityisavain',
notifications_nostr_private_key_desc:
'Yksityinen avain (hex tai nsec) Nostr-viestien lähettämisen allekirjoitukseen',
notifications_nostr_identifiers: 'Nostr-tunnisteet',
notifications_nostr_identifiers_desc:
'Lista tunnisteista kenelle tiedotukset lähetetään',
notifications_telegram_config: 'Telegram-määritykset',
notifications_enable_telegram: 'Käytä Telegram:ia',
notifications_enable_telegram_desc: 'Lähetä tietodukset Telegram:in kautta',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Telegram botin Access token',
notifications_chat_id: 'Keskustelun tunnus',
notifications_chat_id_desc: 'Keskustelun tunnus minne tiedotukset lähetetään',
notifications_email_config: 'Sähköposti määritykset',
notifications_enable_email: 'Käytä sähköpostia',
notifications_enable_email_desc: 'Lähetä tiedotteet sähköpostilla',
notifications_send_test_email: 'Lähetä testiposti',
notifications_send_email: 'Lähetä sähköpostiosoitteella',
notifications_send_email_desc: 'Lähettäjänä näkyvä sähköpostiosoite',
notifications_send_email_username: 'Käyttäjätunnus',
notifications_send_email_username_desc:
'Käyttäjätunnus, mikäli tyhjä, käytetään sähköpostiosoitetta',
notifications_send_email_password: 'Lähtevän sähköpostin salasana',
notifications_send_email_password_desc: 'Salasana lähettävälle sähköpostille',
notifications_send_email_server_port: 'Lähtevän sähköpostin SMTP-portti',
notifications_send_email_server_port_desc: 'SMTP-palvelimen portti',
notifications_send_email_server: 'Lähtevän sähköpostin SMTP-palvelin',
notifications_send_email_server_desc:
'SMTP-palvelin jonka kautta sähköpostit lähetetään',
notifications_send_to_emails: 'Sähköpostien vastaanottaja',
notifications_send_to_emails_desc: 'Kenelle sähköpostit lähetetään',
notification_settings_update: 'Asetuksia päivitetty',
notification_settings_update_desc:
'Tiedota kun palvelimen asetuksia on päivitetty',
notification_server_start_stop: 'Palvelimen Käynnystys/Sammutus',
notification_server_start_stop_desc:
'Tiedota kun palvelin on käynnistetty tai sammutettu',
notification_watchdog_limit: 'Watchdog-raja -tiedote',
notification_watchdog_limit_desc:
'Tiedota kun watchdog-raja on saavutettu (ei vaikuta rahoituslähteeseen)',
notification_server_status: 'Palvelimen tila',
notification_server_status_desc:
'Lähetä säännölliset tiedotteet palvelimen tilasta (anna tiedotusväli tunteina)',
notification_incoming_payment: 'Saapuvat maksut',
notification_incoming_payment_desc:
'Tiedota kun lompakon vastaanottaman ja saapuvan maksun määrä ylittää rajan (sat)',
notification_outgoing_payment: 'Lähtevät maksut',
notification_outgoing_payment_desc:
'Tiedota kun lompakon lähettävän ja maksettavan maksun määrä ylittää rajan (sat)',
notification_credit_debit: 'Hyvitys / Veloitus',
notification_credit_debit_desc:
'Tiedota kun Superuser tekee lompakon hyvitys- tai veloitustapahtumia',
notification_balance_delta_changed: 'Saldon määrän muutos',
notification_balance_delta_changed_desc:
'Tiedota kun solmun ja LNbits saldojen eri poikkeaa edes yhden satoshin. Tämä tarkastus tehdään joka minuuttu.',
enable_watchdog: 'Watchdog-kytkin',
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',
@@ -178,46 +256,57 @@ window.localisation.fi = {
'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]',
'Mikäli rahoituslähteen saldo laskee alle LNbits kokonaissaldon, muutetaan rahoituslähteeksi heti VoidWallet. Päivittämisen jälkeen asetus pitää päivittää manuaalisestsi.',
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',
more: 'laajenna',
less: 'supista',
releases: 'Julkaisut',
killswitch: 'Killswitch',
watchdog: 'Watchdog',
server_logs: 'Palvelimen lokit',
ip_blocker: 'IP-suodatin',
ip_blocker: 'Palvelimen suojaus IP-osoitesuodattimella',
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',
callback_url_rules: 'Callback URL -säännöt',
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
callback_url_rule_hint:
'Callback URL:it (kuten LNURL) tarkistetaan näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
wallet_limiter: 'Lompakon käyttörajoitin',
wallet_config: 'Wallet Config',
wallet_charts: 'Wallet Charts',
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ä)',
'Päivittäin nostettavissa sat maksimi (0 poistaa käytöstä)',
wallet_max_ballance: 'Maksimisaldo (sat) (0 poistaa käytöstä)',
wallet_limit_secs_between_trans:
'Min sekuntia transaktioiden välillä lompakkoa kohden (0 poistaa käytöstä)',
'Tapahtumien välinen minimi (sec) (0 poistaa käytöstä)',
only_incoming_payments_allowed: 'Vain saapuvat maksut sallittuna',
disable_outgoing_payments: 'Poista lähtevät maksut käytöstä',
number_of_requests: 'Pyyntöjen lukumäärä',
time_unit: 'aikayksikkö',
minute: 'minuutti',
settings: 'Asetukset',
second: 'sekunti',
hour: 'tunti',
disable_server_log: 'Poista palvelimen loki käytöstä',
enable_server_log: 'Ota palvelimen loki käyttöön',
disable_server_log: 'Piilota palvelimen loki',
enable_server_log: 'Näytä palvelimen loki',
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',
instant_access_question: 'perinteinen kirjautuminen',
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_oauth: 'Login with',
signin_with_oauth_or: 'or Login with',
signin_with_nostr: 'Kirjaudu Nostr:lla',
signin_with_google: 'Kirjaudu Google-tunnuksella',
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
signin_with_keycloak: 'Kirjaudu Keycloak-tunnuksella',
@@ -225,12 +314,17 @@ window.localisation.fi = {
password: 'Anna uusi salasana',
password_config: 'Salasanan määritys',
password_repeat: 'Toista uusi salasana',
update_password: 'Päivitä salasana',
change_password: 'Vaihda salasana',
update_credentials: 'Päivitä käyttöoikeustiedot',
update_pubkey: 'Päivitä julkinen avain',
set_password: 'Aseta salasana',
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
invalid_password_repeat: 'Salasanat eivät täsmää',
login: 'Kirjaudu',
register: 'Rekisteröidy',
username: 'Käyttäjänimi',
pubkey: 'Julkinen avain',
user_id: 'Käyttäjä ID',
email: 'Sähköposti',
first_name: 'Etunimi',
@@ -242,17 +336,244 @@ window.localisation.fi = {
invalid_username: 'Virheellinen käyttäjänimi',
auth_provider: 'Tunnistamisen toimittaja',
my_account: 'Tilini',
existing_account_question: 'Onkohan sinulla jo tili?',
background_image: 'Taustakuva',
back: 'Takaisin',
logout: 'Poistu',
look_and_feel: 'Kieli ja värit',
api_token: 'API Token',
api_tokens: 'API Tokens',
access_control_list: 'Access Control List',
access_control_list_admin_warning:
'This is an admin account. The generated tokens will have admin privileges.',
new_api_acl: 'New Access Control List',
api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background',
language: 'Kieli',
color_scheme: 'Väriteema',
extension_cost: 'Tämä julkaisu edellyttää vähintään {cost} satsin maksua.',
admin_settings: 'Pääkäyttäjän asetukset',
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat maksua.',
extension_paid_sats: 'Olet jo maksanut {paid_sats} satsia.',
release_details_error: 'Ei voi hakea julkaisun tietoja.',
pay_from_wallet: 'Maksa lompakosta',
wallet_required: 'Lompakko *',
show_qr: 'Näytä QR',
retry_install: 'Yritä asennusta uudelleen',
new_payment: 'Tee uusi maksu',
hide_empty_wallets: 'Piilota tyhjät lompakot'
new_payment: 'Luo uusi maksu',
update_payment: 'Päivitä maksu',
already_paid_question: 'Kenties maksoit jo?',
sell: 'Myy',
sell_require: 'Pyydä maksua laajennuksen käytöstä',
sell_info:
'{name} -laajennuksen aktivointi edellyttää vähintään {amount} sat maksua.',
hide_empty_wallets: 'Piilota tyhjät lompakot',
recheck: 'Tarkista uudelleen',
contributors: 'Avustajat',
license: 'Lisenssi',
reset_key: 'Vaihda avain',
reset_password: 'Vaihda salasana',
border_choices: 'Reunuksen vaihtoehdot',
select_all: 'Valitse kaikki',
nfc_supported: 'NFC on tuettu',
nfc_not_supported: 'NFC:tä ei tueta',
expire_date: 'Vanhenemispäivämäärä:',
hash: 'Tiiviste:',
welcome_lnbits: 'Tervetuloa LNbits-palveluun',
setup_su_account: 'Määritä Superuser-tili alta.',
create_ticker_converter: 'Luo valuuttamuuntimen Ticker',
enable_audit: 'Ota seuranta käyttöön',
recommended: 'Suositeltu',
audit_desc: 'Tallenna HTTP-pyyntöjä seuraavien suodattimien mukaisesti',
audit_record_req: 'Tallenna pyynnön Body',
audit_record_warning: 'Varoitus:',
audit_record_req_warning_1:
'Luottamukselliset tiedot (kuten salasanat) tallennetaan.',
audit_record_req_warning_2: 'Body-datamäätä voi olla iso.',
audit_record_use: 'Käytä varoen!',
audit_ip: 'Tallenna IP-osoite',
audit_ip_desc: 'Tallenna asiakkaan IP-osoite',
audit_path_params: 'Tallenna Path-parametrit',
audit_query_params: 'Tallenna Query-parametrit',
audit_http_methods: 'Tallenna HTTP-menetelmät',
audit_http_methods_hint:
'Luettelo mukaan otettavista HTTP-menetelmistä. Tyhjä luettelo tallettaa kaikki.',
audit_http_methods_label: 'HTTP-metodit',
audit_resp_codes: 'Tallenna HTTP-vastauskoodit',
audit_resp_codes_hint:
'HTTP-koodien lista, jotka sisällytetään (regex-match). Tyhjä luettelo tallettaa kaikki. Esim: 4.*, 5.*',
audit_resp_codes_label: 'HTTP-vastauskoodi (säännöllinen lauseke)',
audit_paths: 'Sisällytä polut',
audit_paths_hint:
'Luettelo poluista, jotka sisällytetään (regex-vastaavuus). Tyhjä luettelo tarkoittaa kaikkia.',
audit_paths_label: 'HTTP-polku (regex)',
audit_paths_exclude: 'Ohita polut',
audit_paths_exclude_hint:
'Lista poluista, jotka jätetään pois (regex-vastaavuus). Tyhjällä listalla mitään ei jätetä pois.',
audit_paths_exclude_label: 'HTTP-polku (regex)',
exchange_providers: 'Vaihtokurssin tarjoajat',
admin_extensions: 'Pääkäyttäjän laajennukset',
admin_extensions_label: 'Pääkäyttäjän laajennukset',
admin_extensions_hint:
'Laajennuksia voi käyttää vain käyttäjä, jolla on pääkäyttäjäoikeudet',
user_default_extensions: 'Käyttäjän oletuslaajennukset',
user_default_extensions_label: 'Käyttäjän laajennukset',
user_default_extensions_hint:
'Laajennukset, jotka otetaan oletusarvoisesti käyttöön kaikille käyttäjille.',
miscellanous: 'Sekalaiset',
misc_disable_extensions: 'Poista laajennukset käytöstä',
misc_disable_extensions_label: 'Poista kaikki laajennukset käytöstä',
misc_hide_api: 'Piilota API',
misc_hide_api_label:
'Piilottaa lompakon rajapinnan, laajennukset voivat valita välittävätkö tästä asetuksesta',
wallets_management: 'Lompakoiden hallinta',
funding_source_info: 'Rahoituslähteen tiedot',
funding_source: 'Rahoituslähde: {wallet_class}',
node_balance: 'Solmun saldo: {balance} sats',
lnbits_balance: 'LNbits-saldo: {balance} sat',
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
node_managment: 'Solmun hallinta',
node_management_not_supported:
'Solmun hallinta ei ole mahdollista valitun rahoituslähteen kanssa.',
toggle_node_ui: 'Solmun käyttöliittymä',
toggle_public_node_ui: 'Julkinen näkymä solmun tietoihin',
toggle_transactions_node_ui:
'Tapahtumat-välilehti (Poista käytöstä suurilla CLN-solmuilla)',
invoice_expiry: 'Laskun vanhenemisaika',
invoice_expiry_label: 'Laskun vanhentuminen (sekunteina)',
fee_reserve: 'Kuluvaraus',
fee_reserve_percent: 'Kuluvaraus prosentteina',
fee_reserve_msats: 'Kuluvaraus milli-sat',
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
payment_wait_time: 'Maksun odotusaika',
payment_wait_time_desc:
'Kuinka pitkään maksua odotetaan saapuvaksi ennenkuin se merkitään Odotetaan -tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua tms',
server_management: 'Palvelimen hallinta',
base_url: 'Palvelimen URL-osoite',
base_url_label: 'Palvelun staattinen pohja-URL',
authentication: 'Käyttäjän todennus',
auth_token_expiry_label: 'Kirjautumisen vanhentumisaika minuutteina',
auth_token_expiry_hint: 'Aika minuuteissa, jossa kirjautuminen vanhenee',
auth_allowed_methods_label: 'Sallitut kirjautumismenetelmät',
auth_allowed_methods_hint: 'Valitse kirjautumismenetelmät',
auth_nostr_label: 'Nostr kutsujen URL',
auth_nostr_hint:
'Asiakkaiden kirjautumiseen käyttämä absoluuttinen URL-osoite.',
auth_google_ci_label: 'Google-asiakastunnus',
auth_google_ci_hint:
'Varmista, että valtuutetut uudelleenohjaus-URI:t sisältävät https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google-asiakassalasana',
auth_gh_client_id_label: 'GitHub-asiakastunnus',
auth_gh_client_id_hint:
'Varmista, että valtuutuksen paluuosoite-URL on asetettu osoitteeseen https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub-asiakassalaisuusavain',
auth_keycloak_label: 'Keycloak-discovery-URL',
auth_keycloak_ci_label: 'Keycloak-asiakastunnus',
auth_keycloak_ci_hint:
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
currency_settings: 'Valuutta-asetukset',
allowed_currencies: 'Käytettävät valuutat',
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
default_account_currency: 'Tilin oletusvaluutta',
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
max_outgoing_payment_amount_desc: 'Enimmäismäärä jonka voi maksaa',
service_fee: 'Palvelumaksut',
service_fee_label: 'Palvelumaksu (%)',
service_fee_hint: 'Tapahtumastakohtainen palvelumaksu (%)',
service_fee_max: 'Palvelumaksun enimmäismäärä',
service_fee_max_label: 'Palvelumaksu max (sat)',
service_fee_max_hint: 'Suurin veloitettava palvelumaksu (sat)',
fee_wallet: 'Palvelumaksujen lompakko',
fee_wallet_label: 'Palvelumaksujen tilityslompakko (lompakon tunnus)',
fee_wallet_hint: 'Lompakon tunnus, johon palvelumaksut tilitetään',
disable_fee: 'Poista maksu käytöstä',
disable_fee_internal: 'Poista palvelumaksu sisäisiltä maksuilta',
disable_fee_internal_desc: 'Poista palvelumaksu sisäisiltä salamaksuilta',
ui_management: 'Käyttöliittymän hallinta',
ui_site_title: 'Sivuston nimi',
ui_changing_remove_lnbits_elements:
' (tämän muuttamalla LNbits elementit poistuvat kotisivulla ja alareunasta)',
ui_site_tagline: 'Sivuston iskulause',
ui_elements_enable: 'Ota käyttöön elementit etusivulla/alareunassa',
ui_elements_disable: 'Poista elementit käytöstä etusivulla/alareunassa',
ui_toggle_elements_tip: "Poista kotisivuelementit kuten 'toimii' jne.",
ui_site_description: 'Sivuston kuvaus',
ui_site_description_hint:
'Käytä tavallista tekstiä, Markdownia tai puhdasta HTML:ää',
ui_default_wallet_name: 'Oletuslompakon nimi',
ui_default_theme: 'Oletusteema',
lnbits_wallet: 'LNbits-lompakko',
denomination: 'Valuutan nimi',
denomination_hint: 'FakeWallet-lompakon valuutan nimi',
denomination_error: 'Valuutta tunnisssa on oltava 3 merkkiä, tai `sat`',
ui_qr_code_logo: 'QR- ja Favicon-logo',
ui_qr_code_logo_hint:
'Anna QR-koodissa ja Faviconissa käytettävän logo-kuvan URL',
ui_custom_image: 'Yksilöity kuva',
ui_custom_image_label: 'Anna yksilöidyn kuvan URL-osoite',
ui_custom_image_hint:
'Yksilöity kuva näytetään aloitus- ja kirjautumissivuilla',
ui_custom_badge: 'Yksilöity tunnus',
ui_custom_badge_label:
"Yksilöity tunnus 'KÄYTÄ VAROVAISUUTTA - LNbits-lompakko on edelleen BETA-versiossa'",
ui_custom_badge_color_label: 'Kustomoidun tunnuksen väri',
themes: 'Teemat',
themes_hint: 'Valitse käyttäjille saatavilla olevat teemat',
custom_logo: 'Mukautettu logo',
custom_logo_hint: 'Logokuvan sisältävä URL-osoite',
ad_space_title: 'Mainospaikan otsikko',
ad_space_title_label: 'Palvelua tukevat ',
ad_slots: 'Mainospaikat',
ad_slots_hint:
'Mainoslinkit ja kuvatiedostopolut CSV-muodossa, lisäosat voivat valita välittävätkö asetuksesta',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Mainokset käytössä',
ads_disabled: 'Mainokset poistettu käytöstä',
user_management: 'Käyttäjänhallinta',
admin_users: 'Pääkäyttäjät',
admin_users_hint: 'Käyttäjät, joilla on pääkäyttäjän oikeudet',
admin_users_label: 'Käyttäjätunnus',
allowed_users: 'Sallitut käyttäjät',
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
allowed_users_label: 'Käyttäjätunnus',
allow_creation_user: 'Salli uusien käyttäjien luominen',
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
new_user_not_allowed: 'Tunnusten luonti on estetty.',
components: 'Komponentit',
long_running_endpoints: 'Top 5 pisimpään yhteydessä ollutta päätepistettä',
http_request_methods: 'HTTP-pyynnön menetelmät',
http_response_codes: 'HTTP-vastaukset koodit',
request_details: 'Pyynnön tiedot',
http_request_details: 'HTTP-pyynnön tiedot',
payment_details: 'Maksun yksityiskohdat',
payment_details_desc: 'Yksityiskohtaisen maksun sisältö',
payments: 'Maksut',
payment_show_internal: 'Näytä sisäiset maksut',
payment_chart_flow: 'Kuukausittainen maksuvirta',
payment_chart_status: 'Maksun Tila',
payment_chart_tx_per_wallet:
'Lompakkokohtaiset tapahtumat (saldo/kappaletta)',
payment_details_back: 'Takaisin Maksuihin',
payment_chart_tags: 'Maksut Tag:eittäin',
payments_balance_in_out: 'Saldo Sisään/Ulos',
payments_count_in_out: 'Tapahtumia Sisään/Ulos',
payments_status_chart: 'Tilakaavio',
payments_tag_chart: 'Tag-kaavio',
payments_balance_chart: 'Saldo-kaavio',
payments_wallets_chart: 'Lompakko-kaavio',
payments_balance_in_out_chart: 'Saldo Sisään/Ulos -kaavio',
payments_count_in_out_chart: 'Lukumäärä Sisään/Ulos -kaavio',
reset_wallet_keys: 'Uusi API-avaimet',
reset_wallet_keys_desc:
'Tämän lompakon API-avaimet uusitaan. Edelliset API-avaimet lakkaavat toimimasta ja uudet luodaan niiden tilalle..',
view_list: 'Näytä lompakot allekain',
view_column: 'Näytä lompakot rinnakkain',
filter_payments: 'Suodata maksuja',
filter_date: 'Suodata päiväyksellä',
websocket_example: 'Websocket esimerkki'
}
+203 -17
View File
@@ -2,8 +2,10 @@ window.localisation.fr = {
confirm: 'Oui',
server: 'Serveur',
theme: 'Thème',
site_customisation: 'Personnalisation du site',
funding: 'Financement',
users: 'Utilisateurs',
audit: 'Audit',
apps: 'Applications',
channels: 'Canaux',
transactions: 'Transactions',
@@ -26,10 +28,11 @@ window.localisation.fr = {
restart: 'Redémarrer le serveur',
save: 'Enregistrer',
save_tooltip: 'Enregistrer vos modifications',
topup: 'Renflouer',
topup_wallet: 'Reflouer un portefeuille',
topup_hint:
"Utilisez l'ID du portefeuille pour recharger n'importe quel portefeuille",
credit_debit: 'Crédit / Débit',
credit_hint: 'Appuyez sur Entrée pour créditer le compte',
credit_label: '{denomination} à créditer',
credit_ok:
'Succès du crédit/débit des fonds virtuels ({amount} sats). Les paiements dépendent des fonds réels sur la source de financement.',
restart_tooltip:
'Redémarrez le serveur pour que les changements prennent effet',
add_funds_tooltip: 'Ajouter des fonds à un portefeuille.',
@@ -38,8 +41,6 @@ window.localisation.fr = {
'Supprimer tous les paramètres et les réinitialiser aux valeurs par défaut.',
download_backup: 'Télécharger la sauvegarde de la base de données',
name_your_wallet: 'Nommez votre portefeuille {name}',
wallet_topup_ok:
'Succès de la création de fonds virtuels ({amount} sats). Les paiements dépendent des fonds réels sur la source de financement.',
paste_invoice_label:
'Coller une facture, une demande de paiement ou un code lnurl *',
lnbits_description:
@@ -47,6 +48,7 @@ window.localisation.fr = {
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.",
wallet: 'Portefeuille :',
wallets: 'Portefeuilles',
add_wallet: 'Ajouter un nouveau portefeuille',
delete_wallet: 'Supprimer le portefeuille',
@@ -75,8 +77,6 @@ window.localisation.fr = {
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',
@@ -93,7 +93,7 @@ window.localisation.fr = {
pay: 'Payer',
memo: 'Mémo',
date: 'Date',
processing_payment: 'Traitement du paiement...',
payment_processing: 'Traitement du paiement...',
not_enough_funds: 'Fonds insuffisants !',
search_by_tag_memo_amount: 'Rechercher par tag, mémo, montant',
invoice_waiting: 'Facture en attente de paiement',
@@ -112,10 +112,16 @@ window.localisation.fr = {
"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: 'Gérer',
exchanges: 'Échanges',
extensions: 'Extensions',
no_extensions: "Vous n'avez installé aucune extension :(",
created: 'Créé',
search_extensions: 'Rechercher des extensions',
extension_sources: "Sources d'extension",
ext_sources_hint:
'Dépôts à partir desquels les extensions peuvent être téléchargées',
ext_sources_label:
"URL source (utilisez uniquement la source officielle de l'extension LNbits et des sources fiables)",
warning: 'Avertissement',
repository: 'Référentiel',
confirm_continue: 'Êtes-vous sûr de vouloir continuer ?',
@@ -124,8 +130,10 @@ window.localisation.fr = {
uninstall: 'Désinstaller',
drop_db: 'Supprimer les données',
enable: 'Activer',
pay_to_enable: 'Payer pour activer',
enable_extension_details: "Activer l'extension pour l'utilisateur actuel",
disable: 'Désactiver',
delete: 'Supprimer',
installed: 'Installé',
activated: 'Activé',
deactivated: 'Désactivé',
@@ -150,15 +158,19 @@ window.localisation.fr = {
"Vous êtes sur le point de supprimer toutes les données de l'extension. Veuillez taper le nom de l'extension pour continuer :",
extension_required_lnbits_version:
'Cette version nécessite au moins LNbits version',
min_version: 'Minimum (inclus)',
max_version: 'Maximum (exclu)',
payment_hash: 'Hash de paiement',
fee: 'Frais',
amount: 'Montant',
amount_sats: 'Montant (sats)',
tag: 'Étiqueter',
unit: 'Unité',
description: 'Description',
expiry: 'Expiration',
webhook: 'Webhook',
payment_proof: 'Preuve de paiement',
update: 'Mettre à jour',
update_available: 'Mise à jour {version} disponible !',
latest_update: 'Vous êtes sur la dernière version {version}.',
notifications: 'Notifications',
@@ -168,12 +180,7 @@ window.localisation.fr = {
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.',
@@ -190,7 +197,6 @@ window.localisation.fr = {
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",
@@ -224,6 +230,7 @@ window.localisation.fr = {
login_to_account: 'Connectez-vous à votre compte',
create_account: 'Créer un compte',
account_settings: 'Paramètres du compte',
signin_with_nostr: 'Continuer avec Nostr',
signin_with_google: 'Connectez-vous avec Google',
signin_with_github: 'Connectez-vous avec GitHub',
signin_with_keycloak: 'Connectez-vous avec Keycloak',
@@ -232,11 +239,14 @@ window.localisation.fr = {
password_config: 'Configuration du mot de passe',
password_repeat: 'Répétition du mot de passe',
change_password: 'Changer le mot de passe',
update_credentials: "Mettre à jour les informations d'identification",
update_pubkey: 'Mettre à jour la clé publique',
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",
pubkey: 'Clé publique',
user_id: 'Identifiant utilisateur',
email: 'E-mail',
first_name: 'Prénom',
@@ -251,14 +261,190 @@ window.localisation.fr = {
back: 'Retour',
logout: 'Déconnexion',
look_and_feel: 'Apparence',
toggle_gradient: 'Basculer le dégradé',
gradient_background: 'Fond en dégradé',
language: 'Langue',
color_scheme: 'Schéma de couleurs',
admin_settings: 'Paramètres administrateur',
extension_cost: 'Cette version nécessite un paiement minimum de {cost} sats.',
extension_paid_sats: 'Vous avez déjà payé {paid_sats} sats.',
release_details_error: "Impossible d'obtenir les détails de la version.",
pay_from_wallet: 'Payer depuis le portefeuille',
wallet_required: 'Portefeuille *',
show_qr: 'Afficher le QR',
retry_install: "Réessayer l'installation",
new_payment: 'Effectuer un nouveau paiement',
hide_empty_wallets: 'Masquer les portefeuilles vides'
update_payment: 'Mettre à jour le paiement',
already_paid_question: 'Avez-vous déjà payé ?',
sell: 'Vendre',
sell_require: "Demander un paiement pour activer l'extension",
sell_info:
"L'extension {name} nécessite un paiement minimum de {amount} sats pour être activée.",
hide_empty_wallets: 'Masquer les portefeuilles vides',
recheck: 'Revérifier',
contributors: 'Contributeurs',
license: 'Licence',
reset_key: 'Réinitialiser la clé',
reset_password: 'Réinitialiser le mot de passe',
border_choices: 'Choix de bordure',
select_all: 'Sélectionner tout',
nfc_supported: 'NFC pris en charge',
nfc_not_supported: 'NFC non pris en charge',
expire_date: "Date d'expiration :",
hash: 'Hash :',
welcome_lnbits: 'Bienvenue à LNbits',
setup_su_account: 'Configurez le compte Superuser ci-dessous.',
create_ticker_converter: 'Créer un convertisseur de code de devise',
enable_audit: "Activer l'audit",
recommended: 'Recommandé',
audit_desc: 'Enregistrer les requêtes HTTP selon les filtres spécifiés',
audit_record_req: 'Enregistrer le corps de la demande',
audit_record_warning: 'Avertissement :',
audit_record_req_warning_1:
'les données confidentielles (comme les mots de passe) seront enregistrées.',
audit_record_req_warning_2:
'le corps de la requête peut être de grande taille.',
audit_record_use: 'Utilisez-le avec précaution.',
audit_ip: "Enregistrer l'adresse IP",
audit_ip_desc: "Enregistrer l'adresse IP du client",
audit_path_params: 'Enregistrer les paramètres de chemin',
audit_query_params: 'Enregistrer les paramètres de la requête',
audit_http_methods: 'Inclure les méthodes HTTP',
audit_http_methods_hint:
'Liste des méthodes HTTP à inclure. Listes vides signifie toutes.',
audit_http_methods_label: 'Méthodes HTTP',
audit_resp_codes: 'Inclure les codes de réponse HTTP',
audit_resp_codes_hint:
'Liste des codes HTTP à inclure (correspondance regex). Les listes vides signifient tout. Ex : 4.*, 5.*',
audit_resp_codes_label: 'Code de réponse HTTP (regex)',
audit_paths: 'Inclure des chemins',
audit_paths_hint:
'Liste des chemins à inclure (correspondance regex). Liste vide signifie tout.',
audit_paths_label: 'Chemin HTTP (regex)',
audit_paths_exclude: 'Exclure les chemins',
audit_paths_exclude_hint:
'Liste des chemins à exclure (correspondance regex). Liste vide signifie aucun.',
audit_paths_exclude_label: 'Chemin HTTP (regex)',
exchange_providers: "Fournisseurs d'échange",
admin_extensions: "Extensions d'administration",
admin_extensions_label: "Extensions d'administration",
admin_extensions_hint:
"Seuls les utilisateurs avec des privilèges d'administrateur peuvent utiliser les extensions.",
user_default_extensions: "Extensions par défaut de l'utilisateur",
user_default_extensions_label: 'Extensions utilisateur',
user_default_extensions_hint:
'Extensions qui seront activées par défaut pour les utilisateurs.',
miscellanous: 'Divers',
misc_disable_extensions: 'Désactiver les extensions',
misc_disable_extensions_label: 'Désactiver toutes les extensions',
misc_hide_api: "Masquer l'API",
misc_hide_api_label:
"Masque l'API du portefeuille, les extensions peuvent choisir de respecter",
wallets_management: 'Gestion des portefeuilles',
funding_source_info: 'Informations sur la source de financement',
funding_source: 'Source de financement : {wallet_class}',
node_balance: 'Solde du nœud : {balance} sats',
lnbits_balance: 'Solde LNbits : {balance} sats',
funding_reserve_percent: 'Pourcentage de Réserve : {percent} %',
node_managment: 'Gestion des nœuds',
node_management_not_supported:
"La gestion des nœuds n'est pas prise en charge par la source de financement active",
toggle_node_ui: 'Interface utilisateur de nœud',
toggle_public_node_ui: 'Interface utilisateur du nœud public',
toggle_transactions_node_ui:
'Onglet des transactions (Désactiver sur les grands nœuds CLN)',
invoice_expiry: 'Expiration de la facture',
invoice_expiry_label: 'Expiration de la facture (secondes)',
fee_reserve: 'Réserve de frais',
fee_reserve_msats: 'Frais de réservation en msats',
fee_reserve_percent: 'Frais de réservation en pourcentage',
server_management: 'Gestion de serveur',
base_url: 'URL de base',
base_url_label: 'URL statique/de base pour le serveur',
authentication: 'Authentification',
auth_token_expiry_label: "Durée d'expiration du jeton (en minutes)",
auth_token_expiry_hint: "Durée en minutes avant l'expiration du jeton",
auth_allowed_methods_label: "Méthodes d'autorisation autorisées",
auth_allowed_methods_hint: "Sélectionnez les méthodes d'autorisation",
auth_nostr_label: 'URL de requête Nostr',
auth_nostr_hint: 'URL absolue que les clients utiliseront pour se connecter.',
auth_google_ci_label: 'ID Client Google',
auth_google_ci_hint:
'Assurez-vous que les URIs de redirection autorisées contiennent https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Secret client Google',
auth_gh_client_id_label: 'Identifiant client GitHub',
auth_gh_client_id_hint:
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/github/token",
auth_gh_client_secret_label: 'Secret Client GitHub',
auth_keycloak_label: 'URL de découverte Keycloak',
auth_keycloak_ci_label: 'ID Client Keycloak',
auth_keycloak_ci_hint:
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/keycloak/token",
auth_keycloak_cs_label: 'Secret client Keycloak',
currency_settings: 'Paramètres de devise',
allowed_currencies: 'Devises autorisées',
allowed_currencies_hint:
'Limiter le nombre de devises fiduciaires disponibles',
default_account_currency: 'Devise par défaut du compte',
default_account_currency_hint: 'Devise par défaut pour la comptabilité',
service_fee_label: 'Frais de service (%)',
service_fee_hint: 'Frais facturés par tx (%)',
service_fee_max_label: 'Frais de service max (sats)',
service_fee_max_hint: 'Frais de service maximum à facturer en (sats)',
fee_wallet: 'Portefeuille de frais',
fee_wallet_label: 'Portefeuille de frais (ID de portefeuille)',
fee_wallet_hint: 'Identifiant de portefeuille pour envoyer des fonds à',
disable_fee: 'Désactiver les frais',
disable_fee_internal:
'Désactiver les frais de service pour les paiements internes',
disable_fee_internal_desc:
'Désactiver les frais de service pour les paiements Lightning internes',
ui_management: "Gestion de l'interface utilisateur",
ui_site_title: 'Titre du site',
ui_site_tagline: 'Slogan du site',
ui_elements_enable: "Activer les éléments sur la page d'accueil",
ui_elements_disable: "Désactiver les éléments sur la page d'accueil",
ui_toggle_elements_tip:
"Supprimer les éléments de la page d'accueil comme 'fonctionne avec', etc.",
ui_site_description: 'Description du site',
ui_site_description_hint:
'Utilisez du texte brut, du Markdown ou du HTML brut',
ui_default_wallet_name: 'Nom par Défaut du Portefeuille',
lnbits_wallet: 'Portefeuille LNbits',
denomination: 'Dénomination',
denomination_hint: 'Le nom du jeton FakeWallet',
ui_qr_code_logo: 'Logo de code QR',
ui_qr_code_logo_hint: "URL de l'image du logo dans le code QR",
ui_custom_badge: 'Badge personnalisé',
ui_custom_badge_label:
"Badge personnalisé 'À UTILISER AVEC PRÉCAUTION - Le portefeuille LNbits est encore en BÊTA'",
ui_custom_badge_color_label: 'Couleur de badge personnalisée',
themes: 'Thèmes',
themes_hint: 'Choisissez des thèmes disponibles pour les utilisateurs',
custom_logo: 'Logo personnalisé',
custom_logo_hint: "URL de l'image du logo",
ad_space_title: "Titre de l'espace publicitaire",
ad_space_title_label: 'Soutenu par',
ad_slots: 'Emplacements publicitaires',
ad_slots_hint:
"URL de l'annonce et chemins des fichiers image au format CSV, les extensions peuvent choisir de respecter",
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Annonces activées',
ads_disabled: 'Publicités désactivées',
user_management: 'Gestion des utilisateurs',
admin_users: 'Utilisateurs administrateurs',
admin_users_hint: "Utilisateurs avec des privilèges d'administration",
admin_users_label: 'Identifiant utilisateur',
allowed_users: 'Utilisateurs autorisés',
allowed_users_hint: 'Seuls ces utilisateurs peuvent utiliser LNbits',
allowed_users_label: 'ID utilisateur',
allow_creation_user: 'Autoriser la création de nouveaux utilisateurs',
allow_creation_user_desc:
'Permettre la création de nouveaux utilisateurs sur la page dindex',
components: 'Composants',
long_running_endpoints: 'Top 5 points de terminaison longue durée',
http_request_methods: 'Méthodes de requête HTTP',
http_response_codes: 'Codes de réponse HTTP',
request_details: 'Détails de la demande',
http_request_details: 'Détails de la requête HTTP'
}
+198 -16
View File
@@ -2,8 +2,10 @@ window.localisation.it = {
confirm: 'Sì',
server: 'Server',
theme: 'Tema',
site_customisation: 'Personalizzazione del sito',
funding: 'Funding',
users: 'Utenti',
audit: 'Verifica',
apps: 'Applicazioni',
channels: 'Canali',
transactions: 'Transazioni',
@@ -26,9 +28,11 @@ window.localisation.it = {
restart: 'Riavvia il server',
save: 'Salva',
save_tooltip: 'Salva le modifiche',
topup: 'Ricarica',
topup_wallet: 'Ricarica un portafoglio',
topup_hint: "Usa l'ID del portafoglio per ricaricare qualsiasi portafoglio",
credit_debit: 'Credito / Debito',
credit_hint: 'Premere Invio per accreditare i fondi',
credit_label: '{denomination} da accreditare',
credit_ok:
'Credito/addebito riuscito di fondi virtuali ({amount} sats). I pagamenti dipendono dai fondi effettivi sulla fonte di finanziamento.',
restart_tooltip: 'Riavvia il server affinché le modifiche abbiano effetto',
add_funds_tooltip: 'Aggiungere fondi a un portafoglio',
reset_defaults: 'Ripristina le impostazioni predefinite',
@@ -36,8 +40,6 @@ window.localisation.it = {
'Cancella tutte le impostazioni e ripristina i valori predefiniti',
download_backup: 'Scarica il backup del database',
name_your_wallet: 'Dai un nome al tuo portafoglio {name}',
wallet_topup_ok:
'Operazione riuscita nella creazione di fondi virtuali ({amount} sats). I pagamenti dipendono dai fondi effettivi sulla fonte di finanziamento.',
paste_invoice_label:
'Incolla una fattura, una richiesta di pagamento o un codice lnurl *',
lnbits_description:
@@ -45,6 +47,7 @@ window.localisation.it = {
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ì.",
wallet: 'Portafoglio:',
wallets: 'Portafogli',
add_wallet: 'Aggiungi un nuovo portafoglio',
delete_wallet: 'Elimina il portafoglio',
@@ -72,8 +75,6 @@ window.localisation.it = {
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',
@@ -90,7 +91,7 @@ window.localisation.it = {
pay: 'Paga',
memo: 'Memo',
date: 'Dati',
processing_payment: 'Elaborazione pagamento...',
payment_processing: 'Elaborazione pagamento...',
not_enough_funds: 'Non ci sono abbastanza fondi!',
search_by_tag_memo_amount: 'Cerca per tag, memo, importo...',
invoice_waiting: 'Fattura in attesa di pagamento',
@@ -109,10 +110,15 @@ window.localisation.it = {
"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: 'Gestisci',
exchanges: 'Scambi',
extensions: 'Estensioni',
no_extensions: 'Non ci sono estensioni installate :(',
created: 'Creato',
search_extensions: 'Estensioni di ricerca',
extension_sources: 'Fonti di estensione',
ext_sources_hint: 'Repository da cui è possibile scaricare le estensioni',
ext_sources_label:
"URL di origine (utilizzare solo la fonte ufficiale dell'estensione LNbits e fonti affidabili)",
warning: 'Attenzione',
repository: 'Deposito',
confirm_continue: 'Sei sicuro di voler continuare?',
@@ -121,8 +127,10 @@ window.localisation.it = {
uninstall: 'Disinstalla',
drop_db: 'Rimuovi Dati',
enable: 'Abilita',
pay_to_enable: 'Paga per abilitare',
enable_extension_details: "Attiva l'estensione per l'utente corrente",
disable: 'Disabilita',
delete: 'Elimina',
installed: 'Installato',
activated: 'Attivato',
deactivated: 'Disattivato',
@@ -147,15 +155,19 @@ window.localisation.it = {
"Stai per rimuovere tutti i dati per l'estensione. Digita il nome dell'estensione per continuare:",
extension_required_lnbits_version:
'Questa versione richiede almeno la versione LNbits',
min_version: 'Minimo (incluso)',
max_version: 'Massimo (escluso)',
payment_hash: 'Hash del pagamento',
fee: 'Tariffa',
amount: 'Importo',
amount_sats: 'Importo (sats)',
tag: 'Etichetta',
unit: 'Unità',
description: 'Descrizione',
expiry: 'Scadenza',
webhook: 'Webhook',
payment_proof: 'Prova di pagamento',
update: 'Aggiorna',
update_available: 'Aggiornamento {version} disponibile!',
latest_update: 'Sei sulla versione più recente {version}.',
notifications: 'Notifiche',
@@ -164,12 +176,6 @@ window.localisation.it = {
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.',
@@ -186,7 +192,6 @@ window.localisation.it = {
more: 'più',
less: 'meno',
releases: 'Pubblicazioni',
killswitch: 'Interruttore di spegnimento',
watchdog: 'Cane da guardia',
server_logs: 'Registri del server',
ip_blocker: 'Blocco IP',
@@ -221,6 +226,7 @@ window.localisation.it = {
login_to_account: 'Accedi al tuo account',
create_account: 'Crea un account',
account_settings: "Impostazioni dell'account",
signin_with_nostr: 'Continua con Nostr',
signin_with_google: 'Accedi con Google',
signin_with_github: 'Accedi con GitHub',
signin_with_keycloak: 'Accedi con Keycloak',
@@ -229,11 +235,14 @@ window.localisation.it = {
password_config: 'Configurazione della password',
password_repeat: 'Ripeti la password',
change_password: 'Cambia Password',
update_credentials: 'Aggiorna credenziali',
update_pubkey: 'Aggiorna chiave pubblica',
set_password: 'Imposta password',
invalid_password: 'La password deve contenere almeno 8 caratteri',
login: 'Accesso',
register: 'Registrati',
username: 'Nome utente',
pubkey: 'Chiave pubblica',
user_id: 'ID utente',
email: 'Email',
first_name: 'Nome',
@@ -248,15 +257,188 @@ window.localisation.it = {
back: 'Indietro',
logout: 'Esci',
look_and_feel: 'Aspetto e Comportamento',
toggle_gradient: 'Attiva/disattiva gradiente',
gradient_background: 'Sfondo sfumato',
language: 'Lingua',
color_scheme: 'Schema dei colori',
admin_settings: 'Impostazioni di amministrazione',
extension_cost:
'Questa versione richiede un pagamento minimo di {cost} satoshi.',
extension_paid_sats: 'Hai già pagato {paid_sats} sats.',
release_details_error: 'Impossibile ottenere i dettagli della versione.',
pay_from_wallet: 'Paga dal Portafoglio',
wallet_required: 'Portafoglio *',
show_qr: 'Mostra QR',
retry_install: 'Riprova Installazione',
new_payment: 'Effettua Nuovo Pagamento',
hide_empty_wallets: 'Nascondi portafogli vuoti'
update_payment: 'Aggiorna Pagamento',
already_paid_question: 'Hai già pagato?',
sell: 'Vendi',
sell_require: "Chiedi il pagamento per abilitare l'estensione",
sell_info:
"L'estensione {name} richiede un pagamento minimo di {amount} sats per essere abilitata.",
hide_empty_wallets: 'Nascondi portafogli vuoti',
recheck: 'Ricontrolla',
contributors: 'Contributori',
license: 'Licenza',
reset_key: 'Reimposta Chiave',
reset_password: 'Reimposta password',
border_choices: 'Scelte del bordo',
select_all: 'Seleziona tutto',
nfc_supported: 'Supportato NFC',
nfc_not_supported: 'NFC non supportato',
expire_date: 'Data di scadenza:',
hash: 'Hash:',
welcome_lnbits: 'Benvenuto in LNbits',
setup_su_account: "Configura l'account Superuser qui sotto.",
create_ticker_converter: 'Crea Convertitore di Simboli di Valuta',
enable_audit: 'Abilita controllo',
recommended: 'Consigliato',
audit_desc: 'Registrare le richieste HTTP secondo i filtri specificati',
audit_record_req: 'Registra il corpo della richiesta',
audit_record_warning: 'Avvertimento:',
audit_record_req_warning_1:
'I dati riservati (come le password) verranno registrati.',
audit_record_req_warning_2:
'il corpo della richiesta può avere grandi dimensioni.',
audit_record_use: 'Usalo con cautela.',
audit_ip: "Registrare l'indirizzo IP",
audit_ip_desc: "Registra l'indirizzo IP del cliente",
audit_path_params: 'Registra i parametri del percorso',
audit_query_params: 'Registrare i parametri di query',
audit_http_methods: 'Includi i metodi HTTP',
audit_http_methods_hint:
'Elenco di metodi HTTP da includere. Liste vuote significano tutti.',
audit_http_methods_label: 'Metodi HTTP',
audit_resp_codes: 'Includere codici di risposta HTTP',
audit_resp_codes_hint:
'Elenco dei codici HTTP da includere (corrispondenza regex). Liste vuote significano tutto. Ad esempio: 4.*, 5.*',
audit_resp_codes_label: 'Codice di risposta HTTP (regex)',
audit_paths: 'Includi percorsi',
audit_paths_hint:
'Elenco dei percorsi da includere (corrispondenza regex). Elenco vuoto significa tutto.',
audit_paths_label: 'Percorso HTTP (regex)',
audit_paths_exclude: 'Escludi percorsi',
audit_paths_exclude_hint:
'Elenco dei percorsi da escludere (corrispondenza regex). Un elenco vuoto significa nessuno.',
audit_paths_exclude_label: 'Percorso HTTP (regex)',
exchange_providers: 'Fornitori di scambio',
admin_extensions: 'Estensioni Admin',
admin_extensions_label: 'Estensioni amministrative',
admin_extensions_hint:
'Solo un utente con privilegi di amministratore può utilizzare le estensioni.',
user_default_extensions: "Estensioni predefinite dell'utente",
user_default_extensions_label: 'Estensioni utente',
user_default_extensions_hint:
'Estensioni che saranno abilitate di default per gli utenti.',
miscellanous: 'Varie',
misc_disable_extensions: 'Disabilita estensioni',
misc_disable_extensions_label: 'Disabilita tutte le estensioni',
misc_hide_api: 'Nascondi API',
misc_hide_api_label:
"Nasconde l'api del portafoglio, le estensioni possono scegliere di onorare",
wallets_management: 'Gestione dei portafogli',
funding_source_info: 'Informazioni sulla fonte di finanziamento',
funding_source: 'Fonte di finanziamento: {wallet_class}',
node_balance: 'Saldo Nodo: {balance} sats',
lnbits_balance: 'Saldo LNbits: {balance} sats',
funding_reserve_percent: 'Riserva Percentuale: {percent} %',
node_managment: 'Gestione dei nodi',
node_management_not_supported:
'La gestione dei nodi non è supportata dalla fonte di finanziamento attiva.',
toggle_node_ui: 'Interfaccia utente del nodo',
toggle_public_node_ui: 'Interfaccia Utente Nodo Pubblico',
toggle_transactions_node_ui:
'Scheda Transazioni (Disabilita su nodi CLN grandi)',
invoice_expiry: 'Scadenza fattura',
invoice_expiry_label: 'Scadenza fattura (secondi)',
fee_reserve: 'Riserva delle commissioni',
fee_reserve_msats: 'Tariffa di prenotazione in msats',
fee_reserve_percent: 'Commissione di riserva in percentuale',
server_management: 'Gestione server',
base_url: 'URL di base',
base_url_label: 'URL statica/base per il server',
authentication: 'Autenticazione',
auth_token_expiry_label: 'Minuti di scadenza del token',
auth_token_expiry_hint: 'Tempo in minuti fino alla scadenza del token',
auth_allowed_methods_label: 'Metodi di autorizzazione consentiti',
auth_allowed_methods_hint: 'Seleziona i metodi di autorizzazione',
auth_nostr_label: 'URL richiesta Nostr',
auth_nostr_hint: 'URL assoluto che i clienti utilizzeranno per accedere.',
auth_google_ci_label: 'ID client di Google',
auth_google_ci_hint:
'Assicurati che gli URI di reindirizzamento autorizzati contengano https://{domain}/api/v1/auth/google/token',
auth_google_cs_label: 'Google Client Secret',
auth_gh_client_id_label: 'ID client di GitHub',
auth_gh_client_id_hint:
"Assicurati che l'URL di callback dell'autorizzazione sia impostato su https://{domain}/api/v1/auth/github/token",
auth_gh_client_secret_label: 'Segreto Client GitHub',
auth_keycloak_label: 'URL di individuazione di Keycloak',
auth_keycloak_ci_label: 'ID client di Keycloak',
auth_keycloak_ci_hint:
"Assicurati che l'URL di callback dell'autorizzazione sia impostato su https://{domain}/api/v1/auth/keycloak/token",
auth_keycloak_cs_label: 'Keycloak Client Secret',
currency_settings: 'Impostazioni valuta',
allowed_currencies: 'Valute consentite',
allowed_currencies_hint: 'Limita il numero di valute fiat disponibili',
default_account_currency: 'Valuta predefinita del conto',
default_account_currency_hint: 'Valuta predefinita per la contabilità',
service_fee_label: 'Tassa di servizio (%)',
service_fee_hint: 'Tariffa addebitata per transazione (%)',
service_fee_max_label: 'Commissione di servizio max (sats)',
service_fee_max_hint: 'Commissione massima da addebitare in (sats)',
fee_wallet: 'Portafoglio delle commissioni',
fee_wallet_label: 'Portafoglio delle commissioni (ID portafoglio)',
fee_wallet_hint: 'ID portafoglio a cui inviare fondi',
disable_fee: 'Disabilita Commissione',
disable_fee_internal:
'Disabilita la commissione di servizio per i pagamenti interni',
disable_fee_internal_desc:
'Disabilita la commissione di servizio per i pagamenti Lightning interni',
ui_management: "Gestione dell'interfaccia utente",
ui_site_title: 'Titolo del sito',
ui_site_tagline: 'Slogan del sito',
ui_elements_enable: 'Abilita elementi sulla homepage',
ui_elements_disable: 'Disabilita elementi sulla homepage',
ui_toggle_elements_tip: "Rimuovi elementi della homepage come 'runs on' ecc.",
ui_site_description: 'Descrizione del sito',
ui_site_description_hint: 'Usa testo normale, Markdown o HTML grezzo',
ui_default_wallet_name: 'Nome predefinito del portafoglio',
lnbits_wallet: 'Portafoglio LNbits',
denomination: 'Denominazione',
denomination_hint: 'Il nome per il token FakeWallet',
ui_qr_code_logo: 'Logo del codice QR',
ui_qr_code_logo_hint: "URL all'immagine del logo nel codice QR",
ui_custom_badge: 'Badge personalizzato',
ui_custom_badge_label:
"Badge personalizzato 'USARE CON CAUTELA - Il portafoglio LNbits è ancora in BETA'",
ui_custom_badge_color_label: 'Colore distintivo personalizzato',
themes: 'Temi',
themes_hint: 'Scegli i temi disponibili per gli utenti',
custom_logo: 'Logo personalizzato',
custom_logo_hint: "URL all'immagine del logo",
ad_space_title: 'Titolo Spazio Pubblicitario',
ad_space_title_label: 'Supportato da',
ad_slots: 'Spazi pubblicitari',
ad_slots_hint:
"Percorso dell'URL e dell'immagine in formato CSV, le estensioni possono scegliere di rispettare",
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Annunci abilitati',
ads_disabled: 'Annunci disabilitati',
user_management: 'Gestione utenti',
admin_users: 'Utenti amministratori',
admin_users_hint: 'Utenti con privilegi di amministratore',
admin_users_label: 'ID utente',
allowed_users: 'Utenti consentiti',
allowed_users_hint: 'Solo questi utenti possono usare LNbits',
allowed_users_label: 'ID utente',
allow_creation_user: 'Consenti la creazione di nuovi utenti',
allow_creation_user_desc:
'Consenti la creazione di nuovi utenti nella pagina indice',
components: 'Componenti',
long_running_endpoints: 'I primi 5 endpoint a lunga esecuzione',
http_request_methods: 'Metodi di richiesta HTTP',
http_response_codes: 'Codici di risposta HTTP',
request_details: 'Dettagli della richiesta',
http_request_details: 'Dettagli della richiesta HTTP'
}
+195 -17
View File
@@ -2,8 +2,10 @@ window.localisation.jp = {
confirm: 'はい',
server: 'サーバー',
theme: 'テーマ',
site_customisation: 'サイトカスタマイズ',
funding: '資金調達',
users: 'ユーザー',
audit: '監査',
apps: 'アプリ',
channels: 'チャンネル',
transactions: 'トランザクション',
@@ -26,23 +28,23 @@ window.localisation.jp = {
restart: 'サーバーを再起動する',
save: '保存',
save_tooltip: '変更を保存する',
topup: 'トップアップ',
topup_wallet: 'ウォレットをトップアップする',
topup_hint: 'ウォレットIDを使用して、任意のウォレットをトップアップできます',
credit_debit: 'クレジット / デビット',
credit_hint:
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
credit_label: '{denomination} をクレジットに',
restart_tooltip: 'サーバーを再起動して変更を適用します',
add_funds_tooltip: 'ウォレットに資金を追加します。',
reset_defaults: 'リセット',
reset_defaults_tooltip: 'すべての設定を削除してデフォルトに戻します。',
download_backup: 'データベースのバックアップをダウンロードする',
name_your_wallet: 'あなたのウォレットの名前 {name}',
wallet_topup_ok:
'仮想資金の作成に成功しました({amount} sats)。支払いは資金ソースの実際の資金に依存します。',
paste_invoice_label: '請求書を貼り付けてください',
lnbits_description:
'簡単にインストールでき、軽量なLNbitsは、あらゆるライトニングネットワークの資金源と、LNbits自身でさえも実行できます!LNbitsを個人で実行することも、他人に対してカストディアンソリューションをで実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
export_to_phone: '電話にエクスポート',
export_to_phone_desc:
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
wallet: 'ウォレット:',
wallets: 'ウォレット',
add_wallet: 'ウォレットを追加',
delete_wallet: 'ウォレットを削除',
@@ -69,9 +71,6 @@ window.localisation.jp = {
api_keys_api_docs: 'ノードURL、APIキー、APIドキュメント',
lnbits_version: 'LNbits バージョン',
runs_on: 'で実行',
credit_hint:
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
credit_label: '{denomination} をクレジットに',
paste: '貼り付け',
paste_from_clipboard: 'クリップボードから貼り付け',
paste_request: 'リクエストを貼り付ける',
@@ -88,7 +87,7 @@ window.localisation.jp = {
pay: '支払う',
memo: 'メモ',
date: '日付',
processing_payment: '支払い処理中',
payment_processing: '支払い処理中',
not_enough_funds: '資金が不足しています',
search_by_tag_memo_amount: 'タグ、メモ、金額で検索',
invoice_waiting: '請求書を待っています',
@@ -106,10 +105,15 @@ window.localisation.jp = {
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。ウォレットを削除する前に、ウォレットをエクスポートしてください。',
no_transactions: 'トランザクションはありません',
manage: '管理',
exchanges: '取引所',
extensions: '拡張機能',
no_extensions: '拡張機能はありません',
created: '作成済み',
search_extensions: '検索拡張機能',
extension_sources: '拡張ソース',
ext_sources_hint: '拡張機能をダウンロードできるリポジトリ',
ext_sources_label:
'ソースURL(公式のLNbits拡張ソースおよび信頼できるソースのみを使用してください)',
warning: '警告',
repository: 'リポジトリ',
confirm_continue: '続行してもよろしいですか?',
@@ -118,8 +122,10 @@ window.localisation.jp = {
uninstall: 'アンインストール',
drop_db: 'データを削除',
enable: '有効',
pay_to_enable: '有効にするために支払う',
enable_extension_details: '現在のユーザーの拡張機能を有効にする',
disable: '無効',
delete: '削除',
installed: 'インストール済み',
activated: '有効化',
deactivated: '無効化',
@@ -144,15 +150,19 @@ window.localisation.jp = {
'エクステンションのすべてのデータを削除しようとしています。続行するには、エクステンションの名前を入力してください:',
extension_required_lnbits_version:
'このリリースには少なくとも LNbits バージョンが必要です',
min_version: '最小値(含む)',
max_version: '最大(除外)',
payment_hash: '支払いハッシュ',
fee: '料金',
amount: '量',
amount_sats: '金額 (サッツ)',
tag: 'タグ',
unit: '単位',
description: '説明',
expiry: '有効期限',
webhook: 'ウェブフック',
payment_proof: '支払い証明',
update: '更新',
update_available: 'アップデート{version}が利用可能です!',
latest_update: 'あなたは最新バージョン{version}を使用しています。',
notifications: '通知',
@@ -161,12 +171,6 @@ window.localisation.jp = {
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に変更します。アップデート後は手動で有効にする必要があります。',
@@ -183,7 +187,6 @@ window.localisation.jp = {
more: 'より多くの',
less: '少ない',
releases: 'リリース',
killswitch: 'キルスイッチ',
watchdog: 'ウォッチドッグ',
server_logs: 'サーバーログ',
ip_blocker: 'IPブロッカー',
@@ -217,6 +220,7 @@ window.localisation.jp = {
login_to_account: 'アカウントにログインしてください',
create_account: 'アカウントを作成',
account_settings: 'アカウント設定',
signin_with_nostr: 'Nostrで続行',
signin_with_google: 'Googleでサインイン',
signin_with_github: 'GitHubでサインイン',
signin_with_keycloak: 'Keycloakでサインイン',
@@ -225,11 +229,14 @@ window.localisation.jp = {
password_config: 'パスワード設定',
password_repeat: 'パスワードの再入力',
change_password: 'パスワードを変更',
update_credentials: '資格情報を更新する',
update_pubkey: '公開鍵を更新',
set_password: 'パスワードを設定',
invalid_password: 'パスワードは少なくとも8文字必要です',
login: 'ログイン',
register: '登録',
username: 'ユーザー名',
pubkey: '公開鍵',
user_id: 'ユーザーID',
email: 'メール',
first_name: '名',
@@ -244,14 +251,185 @@ window.localisation.jp = {
back: '戻る',
logout: 'ログアウト',
look_and_feel: 'ルック・アンド・フィール',
toggle_gradient: 'グラデーションを切り替える',
gradient_background: 'グラデーション背景',
language: '言語',
color_scheme: 'カラースキーム',
admin_settings: '管理設定',
extension_cost: 'このリリースには最低 {cost} サトシの支払いが必要です。',
extension_paid_sats: 'すでに{paid_sats} satsを支払いました。',
release_details_error: 'リリースの詳細を取得できません。',
pay_from_wallet: 'ウォレットから支払う',
wallet_required: 'ウォレット *',
show_qr: 'QRを表示',
retry_install: '再試行インストール',
new_payment: '新しい支払いを作成する',
hide_empty_wallets: '空のウォレットを非表示にする'
update_payment: '支払いを更新する',
already_paid_question: 'すでに支払いましたか?',
sell: '販売する',
sell_require: '拡張を有効にするために支払いを求める',
sell_info:
'{name}拡張機能を有効にするには、最小{amount}サツの支払いが必要です。',
hide_empty_wallets: '空のウォレットを非表示にする',
recheck: '再確認',
contributors: '貢献者',
license: 'ライセンス',
reset_key: 'リセットキー',
reset_password: 'パスワードをリセットする',
border_choices: '境界の選択肢',
select_all: 'すべて選択',
nfc_supported: 'NFC対応',
nfc_not_supported: 'NFCがサポートされていません',
expire_date: '有効期限日:',
hash: 'ハッシュ:',
welcome_lnbits: 'LNbitsへようこそ',
setup_su_account: 'スーパーアカウントを以下に設定してください。',
create_ticker_converter: '通貨ティッカーコンバーターを作成',
enable_audit: '監査を有効にする',
recommended: '推奨',
audit_desc: '指定されたフィルターに従ってHTTPリクエストを記録する',
audit_record_req: 'リクエストボディの記録',
audit_record_warning: '警告:',
audit_record_req_warning_1: 'パスワードなどの機密データが記録されます。',
audit_record_req_warning_2:
'リクエストボディは大きなサイズになる可能性があります。',
audit_record_use: '注意して使用してください。',
audit_ip: 'IPアドレスを記録する',
audit_ip_desc: 'クライアントのIPアドレスを記録する',
audit_path_params: 'パスパラメータを記録',
audit_query_params: 'クエリパラメータを記録する',
audit_http_methods: 'HTTPメソッドを含める',
audit_http_methods_hint:
'含めるHTTPメソッドのリスト。空のリストはすべてを意味します。',
audit_http_methods_label: 'HTTPメソッド',
audit_resp_codes: 'HTTPレスポンスコードを含める',
audit_resp_codes_hint:
'含めるHTTPコードの一覧(正規表現で一致)。空のリストはすべてを意味します。例: 4.*, 5.*',
audit_resp_codes_label: 'HTTPレスポンスコード(正規表現)',
audit_paths: 'パスを含める',
audit_paths_hint:
'含めるパスのリスト(正規表現マッチ)。空のリストはすべてを意味します。',
audit_paths_label: 'HTTP パス (正規表現)',
audit_paths_exclude: 'パスを除外',
audit_paths_exclude_hint:
'除外するパスの一覧(正規表現の一致)。空のリストは対象がないことを意味します。',
audit_paths_exclude_label: 'HTTP パス (正規表現)',
exchange_providers: '取引所プロバイダー',
admin_extensions: '管理拡張機能',
admin_extensions_label: '管理者拡張機能',
admin_extensions_hint: '拡張機能は管理者権限を持つユーザーのみが使用できます',
user_default_extensions: 'ユーザーデフォルト拡張機能',
user_default_extensions_label: 'ユーザー拡張機能',
user_default_extensions_hint:
'ユーザーに対してデフォルトで有効化される拡張機能。',
miscellanous: 'その他',
misc_disable_extensions: '拡張機能を無効にする',
misc_disable_extensions_label: 'すべての拡張機能を無効にする',
misc_hide_api: 'APIを非表示',
misc_hide_api_label:
'ウォレットAPIを隠すことができ、拡張機能は尊重することを選ぶことができます。',
wallets_management: 'ウォレット管理',
funding_source_info: '資金源情報',
funding_source: '資金源: {wallet_class}',
node_balance: 'ノード残高: {balance} サッツ',
lnbits_balance: 'LNbits残高: {balance} sats',
funding_reserve_percent: '予約パーセント: {percent} %',
node_managment: 'ノード管理',
node_management_not_supported:
'アクティブな資金源ではノード管理がサポートされていません',
toggle_node_ui: 'ノードUI',
toggle_public_node_ui: 'パブリックノードUI',
toggle_transactions_node_ui:
'トランザクションタブ(大規模なCLNノードで無効化)',
invoice_expiry: 'インボイスの有効期限',
invoice_expiry_label: 'インボイスの有効期限(秒)',
fee_reserve: '料金予約',
fee_reserve_msats: 'ミリサトシでの予約手数料',
fee_reserve_percent: 'パーセンテージの予約料',
server_management: 'サーバー管理',
base_url: 'ベースURL',
base_url_label: 'サーバーの静的/基本URL',
authentication: '認証',
auth_token_expiry_label: 'トークン有効期限(分)',
auth_token_expiry_hint: 'トークンが失効するまでの時間(分)',
auth_allowed_methods_label: '許可された認証方法',
auth_allowed_methods_hint: '認証方法を選択',
auth_nostr_label: 'Nostr リクエスト URL',
auth_nostr_hint: 'クライアントがログインするために使用する絶対URL。',
auth_google_ci_label: 'Google クライアントID',
auth_google_ci_hint:
'認可されたリダイレクトURIにhttps://{domain}/api/v1/auth/google/tokenが含まれていることを確認してください',
auth_google_cs_label: 'Google クライアントシークレット',
auth_gh_client_id_label: 'GitHub クライアントID',
auth_gh_client_id_hint:
'認証コールバックURLがhttps://{domain}/api/v1/auth/github/tokenに設定されていることを確認してください。',
auth_gh_client_secret_label: 'GitHub クライアントシークレット',
auth_keycloak_label: 'キーコーク ディスカバリー URL',
auth_keycloak_ci_label: 'Keycloak クライアント ID',
auth_keycloak_ci_hint:
'認証コールバックURLが https://{domain}/api/v1/auth/keycloak/token に設定されていることを確認してください。',
auth_keycloak_cs_label: 'キークローククライアントシークレット',
currency_settings: '通貨設定',
allowed_currencies: '許可されている通貨',
allowed_currencies_hint: '利用可能な法定通貨の数を制限する',
default_account_currency: 'デフォルト口座通貨',
default_account_currency_hint: '会計のデフォルト通貨',
service_fee_label: 'サービス料 (%)',
service_fee_hint: '1 取引あたりの手数料 (%)',
service_fee_max_label: 'サービス料最大 (sats)',
service_fee_max_hint: '(サット)での最大サービス料金',
fee_wallet: '手数料ウォレット',
fee_wallet_label: '手数料ウォレット (ウォレットID)',
fee_wallet_hint: '送金先のウォレットID',
disable_fee: '手数料を無効にする',
disable_fee_internal: '内部支払に対するサービス手数料を無効にする',
disable_fee_internal_desc:
'内部のライトニングペイメントのサービス料金を無効にする',
ui_management: 'UI管理',
ui_site_title: 'サイトのタイトル',
ui_site_tagline: 'サイトのタグライン',
ui_elements_enable: 'ホームページの要素を有効にする',
ui_elements_disable: 'ホームページの要素を無効にする',
ui_toggle_elements_tip: '「runs on」などのホームページ要素を削除します。',
ui_site_description: 'サイトの説明',
ui_site_description_hint:
'プレーンテキスト、Markdown、または生のHTMLを使用してください。',
ui_default_wallet_name: 'デフォルトウォレット名',
lnbits_wallet: 'LNbitsウォレット',
denomination: '額面',
denomination_hint: 'FakeWalletトークンの名前',
ui_qr_code_logo: 'QRコードロゴ',
ui_qr_code_logo_hint: 'QRコードのロゴ画像のURL',
ui_custom_badge: 'カスタムバッジ',
ui_custom_badge_label:
'カスタムバッジ「使用に注意 - LNbitsウォレットはまだベータ版です」',
ui_custom_badge_color_label: 'カスタムバッジカラー',
themes: 'テーマ',
themes_hint: 'ユーザーが利用可能なテーマを選択してください',
custom_logo: 'カスタムロゴ',
custom_logo_hint: 'ロゴ画像へのURL',
ad_space_title: '広告スペースのタイトル',
ad_space_title_label: 'サポートされています',
ad_slots: '広告スロット',
ad_slots_hint:
'CSV形式の広告URLと画像ファイルパス、拡張機能は遵守することを選択できます',
ad_slots_label: 'URL;img_light_url;img_dark_url、URL...',
ads_enabled: '広告が有効になっています',
ads_disabled: '広告が無効になっています',
user_management: 'ユーザー管理',
admin_users: '管理者ユーザー',
admin_users_hint: '管理者権限を持つユーザー',
admin_users_label: 'ユーザーID',
allowed_users: '許可されたユーザー',
allowed_users_hint: 'これらのユーザーのみがLNbitsを使用できます。',
allowed_users_label: 'ユーザーID',
allow_creation_user: '新しいユーザーの作成を許可',
allow_creation_user_desc:
'インデックスページで新しいユーザーの作成を許可する',
components: 'コンポーネント',
long_running_endpoints: 'トップ5の長時間実行エンドポイント',
http_request_methods: 'HTTPリクエストメソッド',
http_response_codes: 'HTTPレスポンスコード',
request_details: 'リクエストの詳細',
http_request_details: 'HTTPリクエストの詳細'
}
+191 -16
View File
@@ -2,8 +2,10 @@ window.localisation.kr = {
confirm: '확인',
server: '서버',
theme: '테마',
site_customisation: '사이트 사용자 정의',
funding: '자금',
users: '사용자',
audit: '감사',
apps: '앱',
channels: '채널',
transactions: '거래 내역',
@@ -26,9 +28,11 @@ window.localisation.kr = {
restart: '서버 재시작',
save: '저장',
save_tooltip: '변경 사항 저장',
topup: '자금 추가',
topup_wallet: '지갑에 자금 추가',
topup_hint: '자금을 추가할 지갑의 ID를 넣어주세요',
credit_debit: '크레딧 / 직불카드',
credit_hint: '계정에 자금을 넣으려면 Enter를 눌러주세요',
credit_label: '{denomination} 단위로 충전하기',
credit_ok:
'가상 자금({amount} sats) 입출금 성공. 지불은 자금 출처의 실제 자금에 따라 달라집니다.',
restart_tooltip: '변경 사항을 적용하려면 서버를 재시작해야 합니다.',
add_funds_tooltip: '지갑에 자금을 추가합니다.',
reset_defaults: '기본 설정으로 돌아가기',
@@ -36,14 +40,13 @@ window.localisation.kr = {
'설정했던 내용들을 모두 지우고, 기본 설정으로 돌아갑니다.',
download_backup: '데이터베이스 백업 다운로드',
name_your_wallet: '사용할 {name}지갑의 이름을 정하세요',
wallet_topup_ok:
'성공적으로 가상 자금을 생성했습니다 ({amount} sats). 지급은 자금 원천의 실제 자금에 따라 달라집니다.',
paste_invoice_label: '인보이스, 결제 요청, 혹은 lnurl 코드를 붙여넣으세요 *',
lnbits_description:
'설정이 쉽고 가벼운 LNbits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다, 그리고 다른 LNbits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNbits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNbits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNbits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNbits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
export_to_phone: 'QR 코드를 이용해 모바일 기기로 내보내기',
export_to_phone_desc:
'이 QR 코드는 선택된 지갑의 최대 접근 권한을 가진 전체 URL을 담고 있습니다. 스캔 후, 모바일 기기에서 지갑을 열 수 있습니다.',
wallet: '지갑:',
wallets: '지갑',
add_wallet: '새로운 지갑을 추가합니다',
delete_wallet: '지갑을 삭제합니다',
@@ -70,8 +73,6 @@ window.localisation.kr = {
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: '지불 요청 붙여넣기',
@@ -88,7 +89,7 @@ window.localisation.kr = {
pay: '지불하기',
memo: 'Memo',
date: '일시',
processing_payment: '결제 처리 중...',
payment_processing: '결제 처리 중...',
not_enough_funds: '자금이 부족합니다!',
search_by_tag_memo_amount: '태그, memo, 수량으로 검색하기',
invoice_waiting: '결제를 기다리는 인보이스',
@@ -107,10 +108,15 @@ window.localisation.kr = {
'로그인 기능은 향후 업데이트를 통해 지원될 계획이지만, 현재로써는 이 페이지에 향후 다시 접속하기 위해 북마크 설정하는 것을 잊지 마세요! 이 서비스는 아직 BETA 과정에 있고, LNbits 개발자들은 자금 손실에 대해 전혀 책임을 지지 않습니다.',
no_transactions: '아직 아무런 거래도 이루어지지 않았습니다',
manage: '관리',
exchanges: '거래소',
extensions: '확장 기능',
no_extensions: '아직 설치된 확장 기능들이 없네요 :(',
created: '생성됨',
search_extensions: '확장 기능 검색하기',
extension_sources: '확장 소스',
ext_sources_hint: '확장 프로그램을 다운로드할 수 있는 저장소',
ext_sources_label:
'출처 URL (공식 LNbits 확장 소스만 사용하고, 신뢰할 수 있는 출처를 사용하세요)',
warning: '주의',
repository: '저장소',
confirm_continue: '정말로 계속할까요?',
@@ -119,8 +125,10 @@ window.localisation.kr = {
uninstall: '삭제',
drop_db: '데이터 삭제',
enable: '활성화',
pay_to_enable: '지불하여 활성화',
enable_extension_details: '현재 사용자 계정에 해당 확장 기능을 활성화합니다',
disable: '비활성화',
delete: '삭제',
installed: '설치됨',
activated: '작동됨',
deactivated: '작동 중지',
@@ -144,15 +152,19 @@ window.localisation.kr = {
'해당 확장 기능의 모든 데이터가 영구적으로 삭제될 겁니다. 계속하려면 확장 기능의 이름을 입력해주세요:',
extension_required_lnbits_version:
'이 배포 버전은 더 높은 버전의 lnbits가 설치되어 있어야 합니다.',
min_version: '최소값 (포함됨)',
max_version: '최대값 (제외됨)',
payment_hash: '결제 해쉬값',
fee: '수수료',
amount: '액수',
amount_sats: '금액 (사토시)',
tag: '태그',
unit: '단위',
description: '상세',
expiry: '만료',
webhook: 'Webhook',
payment_proof: 'Payment 증거',
update: '업데이트',
update_available: '{version}으로 업데이트가 가능합니다.',
latest_update: '이미 {version} 버전으로 업데이트되었습니다.',
notifications: '알림',
@@ -161,12 +173,6 @@ window.localisation.kr = {
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으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
@@ -183,7 +189,6 @@ window.localisation.kr = {
more: '더 알아보기',
less: '적게',
releases: '배포 버전들',
killswitch: '비상 정지',
watchdog: '와치독',
server_logs: '서버 로그',
ip_blocker: 'IP 기반 차단기',
@@ -214,6 +219,7 @@ window.localisation.kr = {
login_to_account: '계정에 로그인하세요.',
create_account: '계정 생성',
account_settings: '계정 설정',
signin_with_nostr: 'Nostr로 계속하기',
signin_with_google: 'Google으로 로그인',
signin_with_github: 'GitHub으로 로그인',
signin_with_keycloak: 'Keycloak으로 로그인',
@@ -222,11 +228,14 @@ window.localisation.kr = {
password_config: '비밀번호 설정',
password_repeat: '비밀번호 재입력',
change_password: '비밀번호 변경',
update_credentials: '자격 증명 업데이트',
update_pubkey: '공개 키 업데이트',
set_password: '비밀번호 설정',
invalid_password: '비밀번호는 최소 8자 이상이어야 합니다',
login: '로그인',
register: '등록',
username: '사용자 이름',
pubkey: '공개 키',
user_id: '사용자 ID',
email: '이메일',
first_name: '성명',
@@ -241,14 +250,180 @@ window.localisation.kr = {
back: '뒤로',
logout: '로그아웃',
look_and_feel: '외관과 느낌',
toggle_gradient: '그라디언트 전환',
gradient_background: '그라디언트 배경',
language: '언어',
color_scheme: '색상 구성',
admin_settings: '관리자 설정',
extension_cost: '이 버전은 최소 {cost} sats의 지불이 필요합니다.',
extension_paid_sats: '당신은 이미 {paid_sats} sats를 지불했습니다.',
release_details_error: '릴리스 세부 정보를 가져올 수 없습니다.',
pay_from_wallet: '지갑에서 결제하다',
wallet_required: '지갑 *',
show_qr: 'QR 보기',
retry_install: '다시 설치하세요',
new_payment: '새로운 결제하기',
hide_empty_wallets: '빈 지갑 숨기기'
update_payment: '결제 업데이트',
already_paid_question: '이미 지불하셨나요?',
sell: '판매',
sell_require: '확장을 활성화하려면 결제를 요청하십시오.',
sell_info:
'{name} 확장 기능을 활성화하려면 최소 {amount} 사토시의 결제가 필요합니다.',
hide_empty_wallets: '빈 지갑 숨기기',
recheck: '재확인',
contributors: '기여자',
license: '라이선스',
reset_key: '재설정 키',
reset_password: '비밀번호 재설정',
border_choices: '테두리 선택사항',
select_all: '모두 선택',
nfc_supported: 'NFC 지원됨',
nfc_not_supported: 'NFC 지원되지 않음',
expire_date: '만료 날짜:',
hash: '해시:',
welcome_lnbits: 'LNbits에 오신 것을 환영합니다.',
setup_su_account: '슈퍼유저 계정을 아래에 설정하십시오.',
create_ticker_converter: '통화 티커 변환기 생성',
enable_audit: '감사 활성화',
recommended: '추천됨',
audit_desc: '지정된 필터에 따라 HTTP 요청 기록',
audit_record_req: '레코드 요청 본문',
audit_record_warning: '경고:',
audit_record_req_warning_1: '암호와 같은 기밀 데이터가 기록됩니다.',
audit_record_req_warning_2: '요청 본문은 큰 크기를 가질 수 있습니다.',
audit_record_use: '주의해서 사용하십시오.',
audit_ip: 'IP 주소 기록',
audit_ip_desc: '클라이언트의 IP 주소를 기록하십시오.',
audit_path_params: '경로 매개변수 기록',
audit_query_params: '쿼리 매개변수 기록',
audit_http_methods: 'HTTP 메서드 포함',
audit_http_methods_hint:
'포함할 HTTP 메서드 목록. 목록이 비어 있으면 모두 포함됩니다.',
audit_http_methods_label: 'HTTP 방법',
audit_resp_codes: 'HTTP 응답 코드 포함',
audit_resp_codes_hint:
'포함할 HTTP 코드 목록(정규 표현식 일치). 빈 목록은 모두를 의미합니다. 예: 4.*, 5.*',
audit_resp_codes_label: 'HTTP 응답 코드 (정규식)',
audit_paths: '포함 경로',
audit_paths_hint:
'포함할 경로 목록 (정규 표현식 일치). 빈 목록은 모두를 의미합니다.',
audit_paths_label: 'HTTP 경로 (정규식)',
audit_paths_exclude: '제외 경로',
audit_paths_exclude_hint:
'제외할 경로 목록 (정규 표현식 일치). 빈 목록은 없음을 의미합니다.',
audit_paths_exclude_label: 'HTTP 경로 (정규식)',
exchange_providers: '거래소 공급자',
admin_extensions: '관리자 확장 프로그램',
admin_extensions_label: '관리자 확장 기능',
admin_extensions_hint:
'확장 기능은 관리자 권한이 있는 사용자만 사용할 수 있습니다.',
user_default_extensions: '사용자 기본 확장자',
user_default_extensions_label: '사용자 확장 기능',
user_default_extensions_hint: '사용자에게 기본적으로 활성화될 확장 기능.',
miscellanous: '기타',
misc_disable_extensions: '확장 프로그램 사용 안 함',
misc_disable_extensions_label: '모든 확장 프로그램 비활성화',
misc_hide_api: 'API 숨기기',
misc_hide_api_label: '지갑 API 숨기기, 확장 기능은 준수할 수 있음',
wallets_management: '지갑 관리',
funding_source_info: '자금 출처 정보',
funding_source: '자금 출처: {wallet_class}',
node_balance: '노드 잔액: {balance} 사토시',
lnbits_balance: 'LNbits 잔액: {balance} sats',
funding_reserve_percent: '예약 비율: {percent} %',
node_managment: '노드 관리',
node_management_not_supported:
'활성화된 자금 출처에 의해 노드 관리는 지원되지 않습니다.',
toggle_node_ui: '노드 UI',
toggle_public_node_ui: '공개 노드 UI',
toggle_transactions_node_ui: '트랜잭션 탭 (대형 CLN 노드에서는 비활성화)',
invoice_expiry: '송장 만료',
invoice_expiry_label: '송장 만료 (초)',
fee_reserve: '수수료 예약',
fee_reserve_msats: 'msats의 예약 수수료',
fee_reserve_percent: '예약 수수료(%)',
server_management: '서버 관리',
base_url: '기본 URL',
base_url_label: '서버의 정적/기본 URL',
authentication: '인증',
auth_token_expiry_label: '토큰 만료 시간(분)',
auth_token_expiry_hint: '토큰이 만료되기까지 남은 시간(분)',
auth_allowed_methods_label: '허용된 인증 방법',
auth_allowed_methods_hint: '인증 방법 선택',
auth_nostr_label: 'Nostr 요청 URL',
auth_nostr_hint: '클라이언트가 로그인하는 데 사용할 절대 URL.',
auth_google_ci_label: 'Google 클라이언트 ID',
auth_google_ci_hint:
'허가된 리디렉션 URI에 https://{domain}/api/v1/auth/google/token이 포함되어 있는지 확인하세요.',
auth_google_cs_label: 'Google 클라이언트 시크릿',
auth_gh_client_id_label: 'GitHub 클라이언트 ID',
auth_gh_client_id_hint:
'인가 콜백 URL이 https://{domain}/api/v1/auth/github/token으로 설정되어 있는지 확인하십시오.',
auth_gh_client_secret_label: 'GitHub 클라이언트 비밀키',
auth_keycloak_label: 'Keycloak 디스커버리 URL',
auth_keycloak_ci_label: '키클록 클라이언트 ID',
auth_keycloak_ci_hint:
'승인 콜백 URL이 https://{domain}/api/v1/auth/keycloak/token으로 설정되어 있는지 확인하십시오.',
auth_keycloak_cs_label: 'Keycloak 클라이언트 시크릿',
currency_settings: '통화 설정',
allowed_currencies: '허용되는 통화',
allowed_currencies_hint: '사용 가능한 법정 화폐의 수를 제한하십시오.',
default_account_currency: '기본 계좌 통화',
default_account_currency_hint: '회계 기본 통화',
service_fee_label: '서비스 수수료 (%)',
service_fee_hint: '트랜잭션당 수수료 (%)',
service_fee_max_label: '서비스 수수료 최대 (sats)',
service_fee_max_hint: '(사토시)로 부과할 최대 서비스 요금',
fee_wallet: '수수료 지갑',
fee_wallet_label: '수수료 지갑 (지갑 ID)',
fee_wallet_hint: '자금을 보낼 지갑 ID',
disable_fee: '수수료 비활성화',
disable_fee_internal: '내부 결제에 대한 서비스 요금 비활성화',
disable_fee_internal_desc: '내부 라이트닝 결제에 대한 서비스 요금 비활성화',
ui_management: 'UI 관리',
ui_site_title: '사이트 제목',
ui_site_tagline: '사이트 태그라인',
ui_elements_enable: '홈페이지의 요소 활성화',
ui_elements_disable: '홈페이지의 요소 비활성화',
ui_toggle_elements_tip: "'에 의해 구동됨' 등의 홈페이지 요소 제거",
ui_site_description: '사이트 설명',
ui_site_description_hint:
'일반 텍스트, Markdown, 또는 원시 HTML을 사용하십시오.',
ui_default_wallet_name: '기본 지갑 이름',
lnbits_wallet: 'LNbits 지갑',
denomination: '액면가',
denomination_hint: 'FakeWallet 토큰의 이름',
ui_qr_code_logo: 'QR 코드 로고',
ui_qr_code_logo_hint: 'QR 코드의 로고 이미지 URL',
ui_custom_badge: '맞춤 배지',
ui_custom_badge_label:
"사용자 지정 배지 '주의하여 사용 - LNbits 지갑은 여전히 BETA 상태입니다'",
ui_custom_badge_color_label: '사용자 정의 배지 색상',
themes: '테마',
themes_hint: '사용자가 사용할 수 있는 테마 선택',
custom_logo: '맞춤 로고',
custom_logo_hint: '로고 이미지의 URL',
ad_space_title: '광고 공간 제목',
ad_space_title_label: '지원:',
ad_slots: '광고 슬롯',
ad_slots_hint:
'광고 URL 및 이미지 파일 경로를 CSV 형식으로, 확장자는 준수할 수 있습니다.',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: '광고 활성화됨',
ads_disabled: '광고 비활성화됨',
user_management: '사용자 관리',
admin_users: '관리자 사용자',
admin_users_hint: '관리자 권한이 있는 사용자',
admin_users_label: '사용자 ID',
allowed_users: '허용된 사용자',
allowed_users_hint: 'LNbits는 이 사용자들만 사용할 수 있습니다.',
allowed_users_label: '사용자 ID',
allow_creation_user: '새 사용자 생성 허용',
allow_creation_user_desc: '색인 페이지에서 새 사용자 생성 허용',
components: '구성 요소',
long_running_endpoints: '최상위 5개의 장시간 실행 엔드포인트',
http_request_methods: 'HTTP 요청 메서드',
http_response_codes: 'HTTP 응답 코드',
request_details: '요청 세부사항',
http_request_details: 'HTTP 요청 세부사항'
}
+200 -16
View File
@@ -2,8 +2,10 @@ window.localisation.nl = {
confirm: 'Ja',
server: 'Server',
theme: 'Thema',
site_customisation: 'Site-aanpassing',
funding: 'Financiering',
users: 'Gebruikers',
audit: 'Controle',
apps: 'Apps',
channels: 'Kanalen',
transactions: 'Transacties',
@@ -26,9 +28,11 @@ window.localisation.nl = {
restart: 'Server opnieuw opstarten',
save: 'Opslaan',
save_tooltip: 'Sla uw wijzigingen op',
topup: 'Bijvullen',
topup_wallet: 'Een portemonnee bijvullen',
topup_hint: 'Gebruik de portemonnee-ID om elke portemonnee bij te vullen',
credit_debit: 'Credit / Debet',
credit_hint: 'Druk op Enter om de rekening te crediteren',
credit_label: '{denomination} te crediteren',
credit_ok:
'Succesvol crediteren/debiteren van virtuele gelden ({amount} sats). Betalingen zijn afhankelijk van de werkelijke fondsen op de financieringsbron.',
restart_tooltip:
'Start de server opnieuw op zodat wijzigingen van kracht worden',
add_funds_tooltip: 'Voeg geld toe aan een portemonnee.',
@@ -37,14 +41,13 @@ window.localisation.nl = {
'Wis alle instellingen en herstel de standaardinstellingen.',
download_backup: 'Databaseback-up downloaden',
name_your_wallet: 'Geef je {name} portemonnee een naam',
wallet_topup_ok:
'Succes met het aanmaken van virtuele fondsen ({amount} sats). Betalingen zijn afhankelijk van de werkelijke fondsen op de financieringsbron.',
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 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.',
wallet: 'Wallet:',
wallets: 'Portemonnees',
add_wallet: 'Een nieuwe portemonnee toevoegen',
delete_wallet: 'Portemonnee verwijderen',
@@ -72,8 +75,6 @@ window.localisation.nl = {
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',
@@ -90,7 +91,7 @@ window.localisation.nl = {
pay: 'Betalen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Verwerking betaling...',
payment_processing: 'Verwerking betaling...',
not_enough_funds: 'Onvoldoende saldo!',
search_by_tag_memo_amount: 'Zoeken op tag, memo, bedrag',
invoice_waiting: 'Factuur wachtend op betaling',
@@ -109,10 +110,16 @@ window.localisation.nl = {
'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: 'Beheer',
exchanges: 'Beurzen',
extensions: 'Extensies',
no_extensions: 'Je hebt geen extensies geïnstalleerd :(',
created: 'Aangemaakt',
search_extensions: 'Zoekextensies',
extension_sources: 'Extensiebronnen',
ext_sources_hint:
'Repositories van waar de extensies kunnen worden gedownload',
ext_sources_label:
'Bron-URL (gebruik alleen de officiële LNbits-extensiebron en bronnen die je kunt vertrouwen)',
warning: 'Waarschuwing',
repository: 'Repository',
confirm_continue: 'Weet je zeker dat je wilt doorgaan?',
@@ -121,8 +128,10 @@ window.localisation.nl = {
uninstall: 'Deïnstalleren',
drop_db: 'Gegevens verwijderen',
enable: 'Inschakelen',
pay_to_enable: 'Betalen om te activeren',
enable_extension_details: 'Schakel extensie in voor huidige gebruiker',
disable: 'Uitschakelen',
delete: 'Verwijderen',
installed: 'Geïnstalleerd',
activated: 'Geactiveerd',
deactivated: 'Gedeactiveerd',
@@ -147,15 +156,19 @@ window.localisation.nl = {
'U staat op het punt alle gegevens voor de extensie te verwijderen. Typ de naam van de extensie om door te gaan:',
extension_required_lnbits_version:
'Deze release vereist ten minste LNbits-versie',
min_version: 'Minimum (inbegrepen)',
max_version: 'Maximum (uitgesloten)',
payment_hash: 'Betalings-hash',
fee: 'Kosten',
amount: 'Bedrag',
amount_sats: 'Bedrag (sats)',
tag: 'Label',
unit: 'Eenheid',
description: 'Beschrijving',
expiry: 'Vervaldatum',
webhook: 'Webhook',
payment_proof: 'Betalingsbewijs',
update: 'Bijwerken',
update_available: 'Update {version} beschikbaar!',
latest_update: 'U bent op de nieuwste versie {version}.',
notifications: 'Meldingen',
@@ -164,12 +177,6 @@ window.localisation.nl = {
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.',
@@ -186,7 +193,6 @@ window.localisation.nl = {
more: 'meer',
less: 'minder',
releases: 'Uitgaven',
killswitch: 'Killswitch',
watchdog: 'Waakhond',
server_logs: 'Serverlogboeken',
ip_blocker: 'IP-blokkering',
@@ -220,6 +226,7 @@ window.localisation.nl = {
login_to_account: 'Log in op je account',
create_account: 'Account aanmaken',
account_settings: 'Accountinstellingen',
signin_with_nostr: 'Doorgaan met Nostr',
signin_with_google: 'Inloggen met Google',
signin_with_github: 'Inloggen met GitHub',
signin_with_keycloak: 'Inloggen met Keycloak',
@@ -228,11 +235,14 @@ window.localisation.nl = {
password_config: 'Wachtwoordconfiguratie',
password_repeat: 'Wachtwoord herhalen',
change_password: 'Wachtwoord wijzigen',
update_credentials: 'Referenties bijwerken',
update_pubkey: 'Openbare Sleutel Bijwerken',
set_password: 'Wachtwoord instellen',
invalid_password: 'Wachtwoord moet ten minste 8 tekens bevatten',
login: 'Inloggen',
register: 'Registreren',
username: 'Gebruikersnaam',
pubkey: 'Publieke Sleutel',
user_id: 'Gebruikers-ID',
email: 'E-mail',
first_name: 'Voornaam',
@@ -247,14 +257,188 @@ window.localisation.nl = {
back: 'Terug',
logout: 'Afmelden',
look_and_feel: 'Uiterlijk en gedrag',
toggle_gradient: 'Gradiënt Schakelen',
gradient_background: 'Verloopachtergrond',
language: 'Taal',
color_scheme: 'Kleurenschema',
admin_settings: 'Beheerdersinstellingen',
extension_cost: 'Deze release vereist een betaling van minimaal {cost} sats.',
extension_paid_sats: 'U heeft al {paid_sats} sats betaald.',
release_details_error: 'Kan de gegevens van de release niet ophalen.',
pay_from_wallet: 'Betalen vanuit Portemonnee',
wallet_required: 'Wallet *',
show_qr: 'Toon QR',
retry_install: 'Opnieuw installeren',
new_payment: 'Nieuwe betaling maken',
hide_empty_wallets: 'Verberg lege portemonnees'
update_payment: 'Betaling bijwerken',
already_paid_question: 'Heb je al betaald?',
sell: 'Verkopen',
sell_require: 'Vraag betaling om de extensie te activeren.',
sell_info:
'De {name} extensie vereist een betaling van minimaal {amount} sats om in te schakelen.',
hide_empty_wallets: 'Verberg lege portemonnees',
recheck: 'Opnieuw controleren',
contributors: 'Bijdragers',
license: 'Licentie',
reset_key: 'Hersteltoets',
reset_password: 'Wachtwoord Resetten',
border_choices: 'Randkeuzes',
select_all: 'Alles selecteren',
nfc_supported: 'NFC Ondersteund',
nfc_not_supported: 'NFC niet ondersteund',
expire_date: 'Vervaldatum:',
hash: 'Hash:',
welcome_lnbits: 'Welkom bij LNbits',
setup_su_account: 'Stel het Superuser-account hieronder in.',
create_ticker_converter: 'Maak Valuta Ticker Converter',
enable_audit: 'Audit inschakelen',
recommended: 'Aanbevolen',
audit_desc: 'HTTP-verzoeken vastleggen volgens de opgegeven filters',
audit_record_req: 'Verzoeklichaam registreren',
audit_record_warning: 'Waarschuwing:',
audit_record_req_warning_1:
'vertrouwelijke gegevens (zoals wachtwoorden) worden gelogd.',
audit_record_req_warning_2: 'de aanvraagbody kan een grote omvang hebben.',
audit_record_use: 'Gebruik het met voorzichtigheid.',
audit_ip: 'IP-adres vastleggen',
audit_ip_desc: 'Leg het IP-adres van de klant vast',
audit_path_params: 'Parameters van het pad opnemen',
audit_query_params: 'Queryparameters vastleggen',
audit_http_methods: 'Inclusief HTTP-methoden',
audit_http_methods_hint:
'Lijst van HTTP-methoden die moeten worden opgenomen. Lege lijsten betekenen alles.',
audit_http_methods_label: 'HTTP-methoden',
audit_resp_codes: 'Inclusief HTTP-responscodes',
audit_resp_codes_hint:
'Lijst van op te nemen HTTP-codes (regex-overeenkomst). Lege lijst betekent alles. Bijvoorbeeld: 4.*, 5.*',
audit_resp_codes_label: 'HTTP-responscode (regex)',
audit_paths: 'Inclusiepad',
audit_paths_hint:
'Lijst met paden die moeten worden opgenomen (regex match). Lege lijst betekent alles.',
audit_paths_label: 'HTTP-pad (regex)',
audit_paths_exclude: 'Paden uitsluiten',
audit_paths_exclude_hint:
'Lijst met paden die moeten worden uitgesloten (regex-overeenkomst). Een lege lijst betekent geen.',
audit_paths_exclude_label: 'HTTP-pad (regex)',
exchange_providers: 'Wisselaanbieders',
admin_extensions: 'Beheeruitbreidingen',
admin_extensions_label: 'Beheerdersuitbreidingen',
admin_extensions_hint:
'Alleen gebruikers met beheerdersrechten kunnen extensies gebruiken.',
user_default_extensions: 'Standaardextensies voor gebruikers',
user_default_extensions_label: 'Gebruikersuitbreidingen',
user_default_extensions_hint:
'Extensies die standaard voor de gebruikers worden ingeschakeld.',
miscellanous: 'Diversen',
misc_disable_extensions: 'Extensies uitschakelen',
misc_disable_extensions_label: 'Alle extensies uitschakelen',
misc_hide_api: 'API verbergen',
misc_hide_api_label:
'Verbergt de wallet-API, extensies kunnen ervoor kiezen dit te respecteren',
wallets_management: 'Beheer van portemonnees',
funding_source_info: 'Financieringsbroninfo',
funding_source: 'Financieringsbron: {wallet_class}',
node_balance: 'Node Balans: {balance} sats',
lnbits_balance: 'LNbits Saldo: {balance} sats',
funding_reserve_percent: 'Reservepercentage: {percent} %',
node_managment: 'Nodebeheer',
node_management_not_supported:
'Nodebeheer wordt niet ondersteund door de actieve financieringsbron',
toggle_node_ui: 'Node UI',
toggle_public_node_ui: 'Openbare Node UI',
toggle_transactions_node_ui:
'Transacties Tabblad (Uitschakelen op grote CLN-nodes)',
invoice_expiry: 'Factuurvervaldatum',
invoice_expiry_label: 'Factuurverloop (seconden)',
fee_reserve: 'Toegangsvergoeding Reserve',
fee_reserve_msats: 'Reserveringskosten in msats',
fee_reserve_percent: 'Reserveringskosten in procent',
server_management: 'Serverbeheer',
base_url: 'Basis-URL',
base_url_label: 'Statisch/Basis-URL voor de server',
authentication: 'Authenticatie',
auth_token_expiry_label: 'Token vervalt over minuten',
auth_token_expiry_hint: 'Tijd in minuten totdat de token verloopt',
auth_allowed_methods_label: 'Toegestane autorisatiemethoden',
auth_allowed_methods_hint: 'Selecteer autorisatiemethoden',
auth_nostr_label: 'Nostr Aanvraag-URL',
auth_nostr_hint:
'Absolute URL die de klanten zullen gebruiken om in te loggen.',
auth_google_ci_label: 'Google Client-ID',
auth_google_ci_hint:
"Zorg ervoor dat de geautoriseerde omleidings-URL's https://{domain}/api/v1/auth/google/token bevatten.",
auth_google_cs_label: 'Google Client Secret',
auth_gh_client_id_label: 'GitHub client-ID',
auth_gh_client_id_hint:
'Zorg ervoor dat de autorisatie-callback-URL is ingesteld op https://{domain}/api/v1/auth/github/token',
auth_gh_client_secret_label: 'GitHub Clientgeheim',
auth_keycloak_label: 'Keycloak Ontdekking URL',
auth_keycloak_ci_label: 'Keycloak-client-ID',
auth_keycloak_ci_hint:
'Zorg ervoor dat de autorisatie callback-URL is ingesteld op https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak Clientgeheim',
currency_settings: 'Valuta-instellingen',
allowed_currencies: "Toegestane valuta's",
allowed_currencies_hint: "Beperk het aantal beschikbare fiatvaluta's",
default_account_currency: 'Standaardrekeningvaluta',
default_account_currency_hint: 'Standaardvaluta voor boekhouding',
service_fee_label: 'Servicekosten (%)',
service_fee_hint: 'Toeslag per transactie (%)',
service_fee_max_label: 'Servicekosten max (sats)',
service_fee_max_hint:
'Maximale servicekosten om in rekening te brengen in (sats)',
fee_wallet: 'Kosten Portemonnee',
fee_wallet_label: 'Kosten portemonnee (wallet ID)',
fee_wallet_hint: 'Wallet-ID om geld naar over te maken',
disable_fee: 'Kosten uitschakelen',
disable_fee_internal: 'Servicekosten uitschakelen voor interne betalingen',
disable_fee_internal_desc:
'Dienstenkosten uitschakelen voor interne Lightning-betalingen',
ui_management: 'UI-beheer',
ui_site_title: 'Site titel',
ui_site_tagline: 'Site-slogan',
ui_elements_enable: 'Elementen op de homepage inschakelen',
ui_elements_disable: 'Elementen op de homepage uitschakelen',
ui_toggle_elements_tip:
"Verwijder startpagina-elementen zoals 'werkt op' enz.",
ui_site_description: 'Sitebeschrijving',
ui_site_description_hint: 'Gebruik platte tekst, Markdown, of ruwe HTML',
ui_default_wallet_name: 'Standaard Wallet Naam',
lnbits_wallet: 'LNbits-portemonnee',
denomination: 'Denominatie',
denomination_hint: 'De naam voor de FakeWallet token',
ui_qr_code_logo: 'QR-code-logo',
ui_qr_code_logo_hint: 'URL naar logo-afbeelding in QR-code',
ui_custom_badge: 'Aangepaste badge',
ui_custom_badge_label:
"Aangepaste Badge 'GEBRUIK MET VOORZICHTIGHEID - LNbits-portemonnee is nog in BÈTA'",
ui_custom_badge_color_label: 'Aangepaste Badge Kleur',
themes: "Thema's",
themes_hint: "Kies thema's beschikbaar voor gebruikers",
custom_logo: 'Aangepast logo',
custom_logo_hint: 'URL naar logo-afbeelding',
ad_space_title: 'Advertentieruimte Titel',
ad_space_title_label: 'Ondersteund door',
ad_slots: 'Advertentieblokken',
ad_slots_hint:
'Ad URL en afbeeldingspad in CSV-formaat, extensies kunnen ervoor kiezen te honoreren',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Advertenties ingeschakeld',
ads_disabled: 'Advertenties uitgeschakeld',
user_management: 'Gebruikersbeheer',
admin_users: 'Beheerdersgebruikers',
admin_users_hint: 'Gebruikers met beheerdersrechten',
admin_users_label: 'Gebruikers-ID',
allowed_users: 'Toegestane gebruikers',
allowed_users_hint: 'Alleen deze gebruikers kunnen LNbits gebruiken',
allowed_users_label: 'Gebruikers-ID',
allow_creation_user: 'Sta het aanmaken van nieuwe gebruikers toe',
allow_creation_user_desc:
'Sta de aanmaak van nieuwe gebruikers op de indexpagina toe',
components: 'Componenten',
long_running_endpoints: 'Top 5 langlopende eindpunten',
http_request_methods: 'HTTP-aanvraagmethoden',
http_response_codes: 'HTTP-responscodes',
request_details: 'Aanvraagdetails',
http_request_details: 'HTTP-verzoekdetails'
}

Some files were not shown because too many files have changed in this diff Show More