From f01028eac74c4aa0e31aee732b75e229a8f5568c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 13 Sep 2020 21:31:05 -0300 Subject: [PATCH 1/9] migrate from flask to quart. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit also remove all flaskiness from static file serving. and reference all vendored scripts on the base tempĺate for simplicity. --- Makefile | 8 +- Pipfile | 10 +- Pipfile.lock | 223 +++++++++++------- app.json | 4 +- docs/devs/installation.md | 8 +- lnbits/app.py | 60 ++--- lnbits/commands.py | 2 +- lnbits/core/__init__.py | 6 +- lnbits/core/crud.py | 2 +- lnbits/core/services.py | 2 +- lnbits/core/templates/core/extensions.html | 7 +- lnbits/core/templates/core/index.html | 7 +- lnbits/core/templates/core/wallet.html | 20 +- lnbits/core/views/api.py | 16 +- lnbits/core/views/generic.py | 22 +- lnbits/core/views/lnurl.py | 4 +- lnbits/decorators.py | 21 +- lnbits/ext.py | 6 - lnbits/extensions/amilk/__init__.py | 2 +- lnbits/extensions/amilk/views.py | 11 +- lnbits/extensions/amilk/views_api.py | 10 +- lnbits/extensions/diagonalley/__init__.py | 2 +- lnbits/extensions/diagonalley/views.py | 10 +- lnbits/extensions/diagonalley/views_api.py | 34 +-- lnbits/extensions/events/__init__.py | 2 +- .../events/templates/events/display.html | 1 - .../events/templates/events/register.html | 17 -- .../events/templates/events/ticket.html | 1 - lnbits/extensions/events/views.py | 28 ++- lnbits/extensions/events/views_api.py | 20 +- lnbits/extensions/example/__init__.py | 2 +- lnbits/extensions/example/views.py | 6 +- lnbits/extensions/example/views_api.py | 4 +- lnbits/extensions/lndhub/__init__.py | 2 +- lnbits/extensions/lndhub/decorators.py | 6 +- .../lndhub/templates/lndhub/index.html | 35 +-- lnbits/extensions/lndhub/views.py | 6 +- lnbits/extensions/lndhub/views_api.py | 24 +- lnbits/extensions/lnticket/__init__.py | 2 +- .../lnticket/templates/lnticket/display.html | 1 - lnbits/extensions/lnticket/views.py | 10 +- lnbits/extensions/lnticket/views_api.py | 16 +- lnbits/extensions/lnurlp/__init__.py | 2 +- lnbits/extensions/lnurlp/models.py | 2 +- .../lnurlp/templates/lnurlp/display.html | 1 - .../lnurlp/templates/lnurlp/index.html | 1 - .../lnurlp/templates/lnurlp/print_qr.html | 1 - lnbits/extensions/lnurlp/views.py | 16 +- lnbits/extensions/lnurlp/views_api.py | 14 +- lnbits/extensions/paywall/__init__.py | 2 +- .../paywall/templates/paywall/display.html | 1 - lnbits/extensions/paywall/views.py | 11 +- lnbits/extensions/paywall/views_api.py | 12 +- lnbits/extensions/tpos/__init__.py | 2 +- .../extensions/tpos/templates/tpos/tpos.html | 1 - lnbits/extensions/tpos/views.py | 10 +- lnbits/extensions/tpos/views_api.py | 12 +- lnbits/extensions/usermanager/__init__.py | 2 +- lnbits/extensions/usermanager/views.py | 9 +- lnbits/extensions/usermanager/views_api.py | 29 +-- lnbits/extensions/withdraw/__init__.py | 6 +- lnbits/extensions/withdraw/models.py | 2 +- .../withdraw/templates/withdraw/display.html | 2 - .../withdraw/templates/withdraw/index.html | 9 +- .../withdraw/templates/withdraw/print_qr.html | 12 +- lnbits/extensions/withdraw/views.py | 16 +- lnbits/extensions/withdraw/views_api.py | 16 +- lnbits/settings.py | 3 - lnbits/static/css/base.css | 78 +----- lnbits/templates/base.html | 44 ++-- lnbits/templates/print.html | 15 +- requirements.txt | 44 ++-- 72 files changed, 473 insertions(+), 582 deletions(-) delete mode 100644 lnbits/ext.py diff --git a/Makefile b/Makefile index a95fde2..a48fa1a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: format check +all: format check lnbits/static/css/base.css requirements.txt format: prettier black @@ -18,3 +18,9 @@ checkprettier: $(shell find lnbits -name "*.js" -name ".html") checkblack: $(shell find lnbits -name "*.py") ./venv/bin/black --check lnbits + +lnbits/static/css/base.css: lnbits/static/scss/base.scss + ./venv/bin/pyscss -o lnbits/static/css/base.css lnbits/static/scss/base.scss + +requirements.txt: Pipfile.lock + cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt diff --git a/Pipfile b/Pipfile index 0962b5d..1e81675 100644 --- a/Pipfile +++ b/Pipfile @@ -11,15 +11,15 @@ bitstring = "*" cerberus = "*" ecdsa = "*" environs = "*" -flask = "*" -flask-assets = "*" -flask-compress = "*" -flask-cors = "*" -flask-talisman = "*" lnurl = "*" pyscss = "*" requests = "*" shortuuid = "*" +quart = "*" +quart-cors = "*" +quart-compress = "*" +secure = "*" +typing-extensions = "*" [dev-packages] black = "==20.8b1" diff --git a/Pipfile.lock b/Pipfile.lock index f2e187a..93ae73b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2270f2525e54e976b09491e458033d25ec5bbdea9e74d417e787df33031c6948" + "sha256": "2c716474f9f263d8e1310ca44c2f50996f3516273b483a40e2b2ad68b8071dd6" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "aiofiles": { + "hashes": [ + "sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb", + "sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af" + ], + "version": "==0.5.0" + }, "bech32": { "hashes": [ "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", @@ -31,6 +38,12 @@ "index": "pypi", "version": "==3.1.7" }, + "blinker": { + "hashes": [ + "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" + ], + "version": "==1.4" + }, "brotli": { "hashes": [ "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8", @@ -108,44 +121,41 @@ "index": "pypi", "version": "==8.0.0" }, - "flask": { + "h11": { "hashes": [ - "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", - "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" + "sha256:311dc5478c2568cc07262e0381cdfc5b9c6ba19775905736c87e81ae6662b9fd", + "sha256:9eecfbafc980976dbff26a01dd3487644dd5d00f8038584451fc64a660f7c502" ], - "index": "pypi", - "version": "==1.1.2" + "version": "==0.10.0" }, - "flask-assets": { + "h2": { "hashes": [ - "sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2", - "sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b" + "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5", + "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14" ], - "index": "pypi", - "version": "==2.0" + "version": "==3.2.0" }, - "flask-compress": { + "hpack": { "hashes": [ - "sha256:f367b2b46003dd62be34f7fb1379938032656dca56377a9bc90e7188e4289a7c" + "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", + "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" ], - "index": "pypi", - "version": "==1.5.0" + "version": "==3.0.0" }, - "flask-cors": { + "hypercorn": { "hashes": [ - "sha256:6bcfc100288c5d1bcb1dbb854babd59beee622ffd321e444b05f24d6d58466b8", - "sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324" + "sha256:19f32e7267225c8108ad585b2c5deddf1fe75950797a0e87a682a3a00ef1af95", + "sha256:809d77f3bf9fa0794a598d8dfa0f8d889e7e1c2f927581cd33068803169dc474" ], - "index": "pypi", - "version": "==3.0.9" + "markers": "python_version >= '3.7'", + "version": "==0.10.2" }, - "flask-talisman": { + "hyperframe": { "hashes": [ - "sha256:468131464a249274ed226efc21b372518f442487e58918ccab8357eaa638fd1f", - "sha256:eaa754f4b771dfbe473843391d69643b79e3a38c865790011ac5e4179c68e3ec" + "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", + "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" ], - "index": "pypi", - "version": "==0.7.0" + "version": "==5.2.0" }, "idna": { "hashes": [ @@ -225,6 +235,13 @@ "markers": "python_version >= '3.5'", "version": "==3.7.1" }, + "priority": { + "hashes": [ + "sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe", + "sha256:be4fcb94b5e37cdeb40af5533afe6dd603bd665fe9c8b3052610fc1001d5d1eb" + ], + "version": "==1.3.0" + }, "pydantic": { "hashes": [ "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c", @@ -262,6 +279,30 @@ ], "version": "==0.14.0" }, + "quart": { + "hashes": [ + "sha256:9c634e4c1e4b21b824003c676de1583581258c72b0ac4d2ba747db846e97ff56", + "sha256:d885d782edd9d5dcfd2c4a56e020db3b82493d4c3950f91c221b7d88d239ac93" + ], + "index": "pypi", + "version": "==0.13.1" + }, + "quart-compress": { + "hashes": [ + "sha256:41cd0cc8d26905a45025ddda7022461a71b9d1d950b21b006dc106a1c41c75ef", + "sha256:63af5e6370aa7850fb219d22e1db89965aeb13b8f27bc83e7f9a44118faa3c54" + ], + "index": "pypi", + "version": "==0.2.1" + }, + "quart-cors": { + "hashes": [ + "sha256:020a17d504264db86cada3c1335ef174af28b33f57cee321ddc46d69c33d5c8e", + "sha256:c08bdb326219b6c186d19ed6a97a7fd02de8fe36c7856af889494c69b525c53c" + ], + "index": "pypi", + "version": "==0.3.0" + }, "requests": { "hashes": [ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", @@ -270,6 +311,14 @@ "index": "pypi", "version": "==2.24.0" }, + "secure": { + "hashes": [ + "sha256:4dc8dd4b548831c3ad7f94079332c41d67c781eccc32215ff5a8a49582c1a447", + "sha256:b3bf1e39ebf40040fc3248392343a5052aa14cb45fc87ec91b0bd11f19cc46bd" + ], + "index": "pypi", + "version": "==0.2.1" + }, "shortuuid": { "hashes": [ "sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f", @@ -286,13 +335,20 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + }, "typing-extensions": { "hashes": [ "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], - "markers": "python_version < '3.8'", + "index": "pypi", "version": "==3.7.4.3" }, "urllib3": { @@ -303,13 +359,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.25.10" }, - "webassets": { - "hashes": [ - "sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd", - "sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724" - ], - "version": "==2.0" - }, "werkzeug": { "hashes": [ "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", @@ -317,6 +366,14 @@ ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.0.1" + }, + "wsproto": { + "hashes": [ + "sha256:614798c30e5dc2b3f65acc03d2d50842b97621487350ce79a80a711229edfa9d", + "sha256:e3d190a11d9307112ba23bbe60055604949b172143969c8f641318476a9b6f1d" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==0.15.0" } }, "develop": { @@ -329,11 +386,11 @@ }, "attrs": { "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" + "version": "==20.2.0" }, "black": { "hashes": [ @@ -353,43 +410,43 @@ }, "coverage": { "hashes": [ - "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", - "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", - "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", - "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", - "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", - "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", - "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", - "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", - "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", - "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", - "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", - "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", - "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", - "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", - "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", - "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", - "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", - "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", - "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", - "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", - "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", - "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", - "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", - "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", - "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", - "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", - "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", - "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", - "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", - "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", - "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", - "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", - "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", - "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.2.1" + "version": "==5.3" }, "flake8": { "hashes": [ @@ -407,14 +464,6 @@ "index": "pypi", "version": "==17.8.0" }, - "importlib-metadata": { - "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" - ], - "markers": "python_version < '3.8'", - "version": "==1.7.0" - }, "iniconfig": { "hashes": [ "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", @@ -521,11 +570,11 @@ }, "pytest": { "hashes": [ - "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", - "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" + "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40", + "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043" ], "index": "pypi", - "version": "==6.0.1" + "version": "==6.0.2" }, "pytest-cov": { "hashes": [ @@ -608,16 +657,8 @@ "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], - "markers": "python_version < '3.8'", + "index": "pypi", "version": "==3.7.4.3" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "markers": "python_version >= '3.6'", - "version": "==3.1.0" } } } diff --git a/app.json b/app.json index 96a3f11..a08f9f3 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,5 @@ { "scripts": { - "dokku": { - "predeploy": "flask migrate" - } + "dokku": {} } } diff --git a/docs/devs/installation.md b/docs/devs/installation.md index c4aad25..da53cc3 100644 --- a/docs/devs/installation.md +++ b/docs/devs/installation.md @@ -42,23 +42,19 @@ Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Ne Running the server ------------------ -LNbits uses [Flask][flask] as an application server. +LNbits uses [Quart][quart] as an application server. ```sh $ pipenv run python -m lnbits ``` -There is an environment variable called `FLASK_ENV` that has to be set to `development` -if you want to run Flask in debug mode with autoreload - - Frontend -------- The frontend uses [Vue.js and Quasar][quasar]. -[flask]: http://flask.pocoo.org/ +[quart]: https://pgjones.gitlab.io/ [pipenv]: https://pipenv.pypa.io/ [polar]: https://lightningpolar.com/ [quasar]: https://quasar.dev/start/how-to-use-vue diff --git a/lnbits/app.py b/lnbits/app.py index bf866b5..31edcc1 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,27 +1,28 @@ import importlib -from flask import Flask, g -from flask_assets import Bundle # type: ignore -from flask_cors import CORS # type: ignore -from flask_talisman import Talisman # type: ignore -from werkzeug.middleware.proxy_fix import ProxyFix +from quart import Quart, g +from quart_cors import cors # type: ignore +from quart_compress import Compress # type: ignore +from secure import SecureHeaders # type: ignore -from .commands import flask_migrate +from .commands import db_migrate from .core import core_app from .db import open_db -from .ext import assets, compress from .helpers import get_valid_extensions +secure_headers = SecureHeaders(hsts=False) -def create_app(config_object="lnbits.settings") -> Flask: - """Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. + +def create_app(config_object="lnbits.settings") -> Quart: + """Create application factory. :param config_object: The configuration object to use. """ - app = Flask(__name__, static_folder="static") - app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) # type: ignore + app = Quart(__name__, static_folder="static") app.config.from_object(config_object) - register_flask_extensions(app) + cors(app) + Compress(app) + register_blueprints(app) register_filters(app) register_commands(app) @@ -44,35 +45,11 @@ def register_blueprints(app) -> None: def register_commands(app): """Register Click commands.""" - app.cli.add_command(flask_migrate) - - -def register_flask_extensions(app): - """Register Flask extensions.""" - """If possible we use the .init_app() option so that Blueprints can also use extensions.""" - CORS(app) - Talisman( - app, - force_https=app.config["FORCE_HTTPS"], - content_security_policy={ - "default-src": [ - "'self'", - "'unsafe-eval'", - "'unsafe-inline'", - "blob:", - "api.opennode.co", - ] - }, - ) - - assets.init_app(app) - assets.register("base_css", Bundle("scss/base.scss", filters="pyscss", output="css/base.css")) - compress.init_app(app) + app.cli.add_command(db_migrate) def register_filters(app): """Jinja filters.""" - app.jinja_env.globals["DEBUG"] = app.config["DEBUG"] app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions() app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"] @@ -81,9 +58,14 @@ def register_request_hooks(app): """Open the core db for each request so everything happens in a big transaction""" @app.before_request - def before_request(): + async def before_request(): g.db = open_db() + @app.after_request + async def set_secure_headers(response): + secure_headers.quart(response) + return response + @app.teardown_request - def after_request(exc): + async def after_request(exc): g.db.__exit__(type(exc), exc, None) diff --git a/lnbits/commands.py b/lnbits/commands.py index b6be6f3..60170a9 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -9,7 +9,7 @@ from .helpers import get_valid_extensions @click.command("migrate") -def flask_migrate(): +def db_migrate(): migrate_databases() diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py index 5af72f9..e35d66d 100644 --- a/lnbits/core/__init__.py +++ b/lnbits/core/__init__.py @@ -1,7 +1,9 @@ -from flask import Blueprint +from quart import Blueprint -core_app: Blueprint = Blueprint("core", __name__, template_folder="templates", static_folder="static") +core_app: Blueprint = Blueprint( + "core", __name__, template_folder="templates", static_folder="static", static_url_path="/core/static" +) from .views.api import * # noqa diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 35f1019..6d19c90 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -2,7 +2,7 @@ import json import datetime from uuid import uuid4 from typing import List, Optional, Dict -from flask import g +from quart import g from lnbits import bolt11 from lnbits.settings import DEFAULT_WALLET_NAME diff --git a/lnbits/core/services.py b/lnbits/core/services.py index fd42aa6..e16b2f2 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,5 +1,5 @@ from typing import Optional, Tuple, Dict -from flask import g +from quart import g try: from typing import TypedDict # type: ignore diff --git a/lnbits/core/templates/core/extensions.html b/lnbits/core/templates/core/extensions.html index 8c63c49..8f7e92b 100644 --- a/lnbits/core/templates/core/extensions.html +++ b/lnbits/core/templates/core/extensions.html @@ -1,8 +1,7 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block scripts %} {{ window_vars(user) }} {% assets filters='rjsmin', -output='__bundle__/core/extensions.js', 'core/js/extensions.js' %} - -{% endassets %} {% endblock %} {% block page %} +%} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} {% block page %}
-{% endassets %} {% endblock %} {% block page %} +{% extends "public.html" %} {% block scripts %} + +{% endblock %} {% block page %}
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index b3370bc..5c286ba 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -1,21 +1,7 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block styles %} - -{% endblock %} {% block scripts %} {{ window_vars(user, wallet) }} - -{% assets filters='rjsmin', output='__bundle__/core/chart.js', -'vendor/moment@2.27.0/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %} - -{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js', -'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js', -'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js', 'core/js/wallet.js' -%} - -{% endassets %} {% endblock %} {% block page %} +%} {% block scripts %} {{ window_vars(user, wallet) }} + +{% endblock %} {% block page %}
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index e4cb4fa..3ca6eff 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -1,4 +1,4 @@ -from flask import g, jsonify, request +from quart import g, jsonify, request from http import HTTPStatus from binascii import unhexlify @@ -12,7 +12,7 @@ from lnbits.settings import WALLET @core_app.route("/api/v1/payments", methods=["GET"]) @api_check_wallet_key("invoice") -def api_payments(): +async def api_payments(): if "check_pending" in request.args: delete_expired_invoices() @@ -33,7 +33,7 @@ def api_payments(): "description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"}, } ) -def api_payments_create_invoice(): +async def api_payments_create_invoice(): if "description_hash" in g.data: description_hash = unhexlify(g.data["description_hash"]) memo = "" @@ -65,7 +65,7 @@ def api_payments_create_invoice(): @api_check_wallet_key("admin") @api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}}) -def api_payments_pay_invoice(): +async def api_payments_pay_invoice(): try: payment_hash = pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"]) except ValueError as e: @@ -91,15 +91,15 @@ def api_payments_pay_invoice(): @core_app.route("/api/v1/payments", methods=["POST"]) @api_validate_post_request(schema={"out": {"type": "boolean", "required": True}}) -def api_payments_create(): +async def api_payments_create(): if g.data["out"] is True: - return api_payments_pay_invoice() - return api_payments_create_invoice() + return await api_payments_pay_invoice() + return await api_payments_create_invoice() @core_app.route("/api/v1/payments/", methods=["GET"]) @api_check_wallet_key("invoice") -def api_payment(payment_hash): +async def api_payment(payment_hash): payment = g.wallet.get_payment(payment_hash) if not payment: diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 65b1dbf..36720d9 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -1,4 +1,4 @@ -from flask import g, abort, redirect, request, render_template, send_from_directory, url_for +from quart import g, abort, redirect, request, render_template, send_from_directory, url_for from http import HTTPStatus from os import path @@ -16,19 +16,19 @@ from ..crud import ( @core_app.route("/favicon.ico") -def favicon(): - return send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico") +async def favicon(): + return await send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico") @core_app.route("/") -def home(): - return render_template("core/index.html", lnurl=request.args.get("lightning", None)) +async def home(): + return await render_template("core/index.html", lnurl=request.args.get("lightning", None)) @core_app.route("/extensions") @validate_uuids(["usr"], required=True) @check_user_exists() -def extensions(): +async def extensions(): extension_to_enable = request.args.get("enable", type=str) extension_to_disable = request.args.get("disable", type=str) @@ -40,12 +40,12 @@ def extensions(): elif extension_to_disable: update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0) - return render_template("core/extensions.html", user=get_user(g.user.id)) + return await render_template("core/extensions.html", user=get_user(g.user.id)) @core_app.route("/wallet") @validate_uuids(["usr", "wal"]) -def wallet(): +async def wallet(): user_id = request.args.get("usr", type=str) wallet_id = request.args.get("wal", type=str) wallet_name = request.args.get("nme", type=str) @@ -76,13 +76,15 @@ def wallet(): if wallet_id not in user.wallet_ids: abort(HTTPStatus.FORBIDDEN, "Not your wallet.") - return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee) + return await render_template( + "core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee + ) @core_app.route("/deletewallet") @validate_uuids(["usr", "wal"], required=True) @check_user_exists() -def deletewallet(): +async def deletewallet(): wallet_id = request.args.get("wal", type=str) user_wallet_ids = g.user.wallet_ids diff --git a/lnbits/core/views/lnurl.py b/lnbits/core/views/lnurl.py index 97cbe2c..3ec0fc0 100644 --- a/lnbits/core/views/lnurl.py +++ b/lnbits/core/views/lnurl.py @@ -1,6 +1,6 @@ import requests -from flask import abort, redirect, request, url_for +from quart import abort, redirect, request, url_for from http import HTTPStatus from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore from lnurl.exceptions import LnurlException # type: ignore @@ -13,7 +13,7 @@ from ..crud import create_account, get_user, create_wallet, create_payment @core_app.route("/lnurlwallet") -def lnurlwallet(): +async def lnurlwallet(): memo = "LNbits LNURL funding" try: diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 8f09089..ac73e4e 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -1,5 +1,5 @@ from cerberus import Validator # type: ignore -from flask import g, abort, jsonify, request +from quart import g, abort, jsonify, request from functools import wraps from http import HTTPStatus from typing import List, Union @@ -12,7 +12,7 @@ from lnbits.settings import LNBITS_ALLOWED_USERS def api_check_wallet_key(key_type: str = "invoice"): def wrap(view): @wraps(view) - def wrapped_view(**kwargs): + async def wrapped_view(**kwargs): try: g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type) except KeyError: @@ -24,7 +24,7 @@ def api_check_wallet_key(key_type: str = "invoice"): if not g.wallet: return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED - return view(**kwargs) + return await view(**kwargs) return wrapped_view @@ -34,7 +34,7 @@ def api_check_wallet_key(key_type: str = "invoice"): def api_validate_post_request(*, schema: dict): def wrap(view): @wraps(view) - def wrapped_view(**kwargs): + async def wrapped_view(**kwargs): if "application/json" not in request.headers["Content-Type"]: return ( jsonify({"message": "Content-Type must be `application/json`."}), @@ -42,7 +42,8 @@ def api_validate_post_request(*, schema: dict): ) v = Validator(schema) - g.data = {key: request.json[key] for key in schema.keys() if key in request.json} + data = await request.get_json() + g.data = {key: data[key] for key in schema.keys() if key in data} if not v.validate(g.data): return ( @@ -50,7 +51,7 @@ def api_validate_post_request(*, schema: dict): HTTPStatus.BAD_REQUEST, ) - return view(**kwargs) + return await view(**kwargs) return wrapped_view @@ -60,13 +61,13 @@ def api_validate_post_request(*, schema: dict): def check_user_exists(param: str = "usr"): def wrap(view): @wraps(view) - def wrapped_view(**kwargs): + async def wrapped_view(**kwargs): g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.") if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS: abort(HTTPStatus.UNAUTHORIZED, "User not authorized.") - return view(**kwargs) + return await view(**kwargs) return wrapped_view @@ -76,7 +77,7 @@ def check_user_exists(param: str = "usr"): def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4): def wrap(view): @wraps(view) - def wrapped_view(**kwargs): + async def wrapped_view(**kwargs): query_params = {param: request.args.get(param, type=str) for param in params} for param, value in query_params.items(): @@ -89,7 +90,7 @@ def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = Fals except ValueError: abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.") - return view(**kwargs) + return await view(**kwargs) return wrapped_view diff --git a/lnbits/ext.py b/lnbits/ext.py deleted file mode 100644 index 74e384b..0000000 --- a/lnbits/ext.py +++ /dev/null @@ -1,6 +0,0 @@ -from flask_assets import Environment # type: ignore -from flask_compress import Compress # type: ignore - - -assets = Environment() -compress = Compress() diff --git a/lnbits/extensions/amilk/__init__.py b/lnbits/extensions/amilk/__init__.py index ea93c98..182f023 100644 --- a/lnbits/extensions/amilk/__init__.py +++ b/lnbits/extensions/amilk/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint +from quart import Blueprint amilk_ext: Blueprint = Blueprint("amilk", __name__, static_folder="static", template_folder="templates") diff --git a/lnbits/extensions/amilk/views.py b/lnbits/extensions/amilk/views.py index 11c86f0..fa214e3 100644 --- a/lnbits/extensions/amilk/views.py +++ b/lnbits/extensions/amilk/views.py @@ -1,4 +1,4 @@ -from flask import g, abort, render_template +from quart import g, abort, render_template from http import HTTPStatus from lnbits.decorators import check_user_exists, validate_uuids @@ -10,12 +10,11 @@ from .crud import get_amilk @amilk_ext.route("/") @validate_uuids(["usr"], required=True) @check_user_exists() -def index(): - return render_template("amilk/index.html", user=g.user) +async def index(): + return await render_template("amilk/index.html", user=g.user) @amilk_ext.route("/") -def wall(amilk_id): +async def wall(amilk_id): amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.") - - return render_template("amilk/wall.html", amilk=amilk) + return await render_template("amilk/wall.html", amilk=amilk) diff --git a/lnbits/extensions/amilk/views_api.py b/lnbits/extensions/amilk/views_api.py index 816ca99..c61a4fc 100644 --- a/lnbits/extensions/amilk/views_api.py +++ b/lnbits/extensions/amilk/views_api.py @@ -1,5 +1,5 @@ import requests -from flask import g, jsonify, request, abort +from quart import g, jsonify, request, abort from http import HTTPStatus from lnurl import LnurlWithdrawResponse, handle as handle_lnurl from lnurl.exceptions import LnurlException @@ -15,7 +15,7 @@ from .crud import create_amilk, get_amilk, get_amilks, delete_amilk @amilk_ext.route("/api/v1/amilk", methods=["GET"]) @api_check_wallet_key("invoice") -def api_amilks(): +async def api_amilks(): wallet_ids = [g.wallet.id] if "all_wallets" in request.args: @@ -25,7 +25,7 @@ def api_amilks(): @amilk_ext.route("/api/v1/amilk/milk/", methods=["GET"]) -def api_amilkit(amilk_id): +async def api_amilkit(amilk_id): milk = get_amilk(amilk_id) memo = milk.id @@ -66,7 +66,7 @@ def api_amilkit(amilk_id): "amount": {"type": "integer", "min": 0, "required": True}, } ) -def api_amilk_create(): +async def api_amilk_create(): amilk = create_amilk(wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"]) return jsonify(amilk._asdict()), HTTPStatus.CREATED @@ -74,7 +74,7 @@ def api_amilk_create(): @amilk_ext.route("/api/v1/amilk/", methods=["DELETE"]) @api_check_wallet_key("invoice") -def api_amilk_delete(amilk_id): +async def api_amilk_delete(amilk_id): amilk = get_amilk(amilk_id) if not amilk: diff --git a/lnbits/extensions/diagonalley/__init__.py b/lnbits/extensions/diagonalley/__init__.py index c3eaf52..41afebb 100644 --- a/lnbits/extensions/diagonalley/__init__.py +++ b/lnbits/extensions/diagonalley/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint +from quart import Blueprint diagonalley_ext: Blueprint = Blueprint("diagonalley", __name__, static_folder="static", template_folder="templates") diff --git a/lnbits/extensions/diagonalley/views.py b/lnbits/extensions/diagonalley/views.py index 41a19f5..6781a99 100644 --- a/lnbits/extensions/diagonalley/views.py +++ b/lnbits/extensions/diagonalley/views.py @@ -1,15 +1,11 @@ -import json - -from flask import g, abort, render_template, jsonify +from quart import g, render_template from lnbits.decorators import check_user_exists, validate_uuids from lnbits.extensions.diagonalley import diagonalley_ext -from lnbits.db import open_ext_db @diagonalley_ext.route("/") @validate_uuids(["usr"], required=True) @check_user_exists() -def index(): - - return render_template("diagonalley/index.html", user=g.user) +async def index(): + return await render_template("diagonalley/index.html", user=g.user) diff --git a/lnbits/extensions/diagonalley/views_api.py b/lnbits/extensions/diagonalley/views_api.py index fbdc2a7..9ded7c2 100644 --- a/lnbits/extensions/diagonalley/views_api.py +++ b/lnbits/extensions/diagonalley/views_api.py @@ -1,4 +1,4 @@ -from flask import g, jsonify, request +from quart import g, jsonify, request from http import HTTPStatus from lnbits.core.crud import get_user @@ -18,19 +18,19 @@ from .crud import ( create_diagonalleys_order, get_diagonalleys_order, get_diagonalleys_orders, - delete_diagonalleys_order, + update_diagonalleys_product, ) from lnbits.core.services import create_invoice from base64 import urlsafe_b64encode from uuid import uuid4 from lnbits.db import open_ext_db -###Products +### Products @diagonalley_ext.route("/api/v1/diagonalley/products", methods=["GET"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalley_products(): +async def api_diagonalley_products(): wallet_ids = [g.wallet.id] if "all_wallets" in request.args: @@ -52,7 +52,7 @@ def api_diagonalley_products(): "quantity": {"type": "integer", "min": 0, "required": True}, } ) -def api_diagonalley_product_create(product_id=None): +async def api_diagonalley_product_create(product_id=None): if product_id: product = get_diagonalleys_indexer(product_id) @@ -72,7 +72,7 @@ def api_diagonalley_product_create(product_id=None): @diagonalley_ext.route("/api/v1/diagonalley/products/", methods=["DELETE"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalley_products_delete(product_id): +async def api_diagonalley_products_delete(product_id): product = get_diagonalleys_product(product_id) if not product: @@ -91,7 +91,7 @@ def api_diagonalley_products_delete(product_id): @diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["GET"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalley_indexers(): +async def api_diagonalley_indexers(): wallet_ids = [g.wallet.id] if "all_wallets" in request.args: @@ -114,7 +114,7 @@ def api_diagonalley_indexers(): "zone2cost": {"type": "integer", "min": 0, "required": True}, } ) -def api_diagonalley_indexer_create(indexer_id=None): +async def api_diagonalley_indexer_create(indexer_id=None): if indexer_id: indexer = get_diagonalleys_indexer(indexer_id) @@ -134,7 +134,7 @@ def api_diagonalley_indexer_create(indexer_id=None): @diagonalley_ext.route("/api/v1/diagonalley/indexers/", methods=["DELETE"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalley_indexer_delete(indexer_id): +async def api_diagonalley_indexer_delete(indexer_id): indexer = get_diagonalleys_indexer(indexer_id) if not indexer: @@ -153,7 +153,7 @@ def api_diagonalley_indexer_delete(indexer_id): @diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["GET"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalley_orders(): +async def api_diagonalley_orders(): wallet_ids = [g.wallet.id] if "all_wallets" in request.args: @@ -173,14 +173,14 @@ def api_diagonalley_orders(): "shippingzone": {"type": "integer", "empty": False, "required": True}, } ) -def api_diagonalley_order_create(): +async def api_diagonalley_order_create(): order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data) return jsonify(order._asdict()), HTTPStatus.CREATED @diagonalley_ext.route("/api/v1/diagonalley/orders/", methods=["DELETE"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalley_order_delete(order_id): +async def api_diagonalley_order_delete(order_id): order = get_diagonalleys_order(order_id) if not order: @@ -196,7 +196,7 @@ def api_diagonalley_order_delete(order_id): @diagonalley_ext.route("/api/v1/diagonalley/orders/paid/", methods=["GET"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalleys_order_paid(order_id): +async def api_diagonalleys_order_paid(order_id): with open_ext_db("diagonalley") as db: db.execute( "UPDATE orders SET paid = ? WHERE id = ?", @@ -210,7 +210,7 @@ def api_diagonalleys_order_paid(order_id): @diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/", methods=["GET"]) @api_check_wallet_key(key_type="invoice") -def api_diagonalleys_order_shipped(order_id): +async def api_diagonalleys_order_shipped(order_id): with open_ext_db("diagonalley") as db: db.execute( "UPDATE orders SET shipped = ? WHERE id = ?", @@ -228,7 +228,7 @@ def api_diagonalleys_order_shipped(order_id): @diagonalley_ext.route("/api/v1/diagonalley/stall/products/", methods=["GET"]) -def api_diagonalleys_stall_products(indexer_id): +async def api_diagonalleys_stall_products(indexer_id): with open_ext_db("diagonalley") as db: rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,)) print(rows[1]) @@ -246,7 +246,7 @@ def api_diagonalleys_stall_products(indexer_id): @diagonalley_ext.route("/api/v1/diagonalley/stall/checkshipped/", methods=["GET"]) -def api_diagonalleys_stall_checkshipped(checking_id): +async def api_diagonalleys_stall_checkshipped(checking_id): with open_ext_db("diagonalley") as db: rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,)) @@ -266,7 +266,7 @@ def api_diagonalleys_stall_checkshipped(checking_id): "shippingzone": {"type": "integer", "empty": False, "required": True}, } ) -def api_diagonalley_stall_order(indexer_id): +async def api_diagonalley_stall_order(indexer_id): product = get_diagonalleys_product(g.data["id"]) shipping = get_diagonalleys_indexer(indexer_id) diff --git a/lnbits/extensions/events/__init__.py b/lnbits/extensions/events/__init__.py index 52d499e..abd4895 100644 --- a/lnbits/extensions/events/__init__.py +++ b/lnbits/extensions/events/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint +from quart import Blueprint events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates") diff --git a/lnbits/extensions/events/templates/events/display.html b/lnbits/extensions/events/templates/events/display.html index 542feb9..927c68f 100644 --- a/lnbits/extensions/events/templates/events/display.html +++ b/lnbits/extensions/events/templates/events/display.html @@ -82,7 +82,6 @@
{% endblock %} {% block scripts %} - -{% assets filters='rjsmin', output='__bundle__/core/chart.js', -'vendor/moment@2.27.0/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %} - -{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js', -'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js', -'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js' %} - -{% endassets %} - {% endblock %} diff --git a/lnbits/extensions/lndhub/views.py b/lnbits/extensions/lndhub/views.py index 81e4c69..e9478ff 100644 --- a/lnbits/extensions/lndhub/views.py +++ b/lnbits/extensions/lndhub/views.py @@ -1,4 +1,4 @@ -from flask import render_template, g +from quart import render_template, g from lnbits.decorators import check_user_exists, validate_uuids from lnbits.extensions.lndhub import lndhub_ext @@ -7,5 +7,5 @@ from lnbits.extensions.lndhub import lndhub_ext @lndhub_ext.route("/") @validate_uuids(["usr"], required=True) @check_user_exists() -def lndhub_index(): - return render_template("lndhub/index.html", user=g.user) +async def lndhub_index(): + return await render_template("lndhub/index.html", user=g.user) diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py index 92fc6e5..c413c46 100644 --- a/lnbits/extensions/lndhub/views_api.py +++ b/lnbits/extensions/lndhub/views_api.py @@ -1,6 +1,6 @@ import time from base64 import urlsafe_b64encode -from flask import jsonify, g, request +from quart import jsonify, g, request from lnbits.core.services import pay_invoice, create_invoice from lnbits.core.crud import delete_expired_invoices @@ -14,7 +14,7 @@ from .utils import to_buffer, decoded_as_lndhub @lndhub_ext.route("/ext/getinfo", methods=["GET"]) -def lndhub_getinfo(): +async def lndhub_getinfo(): return jsonify({"error": True, "code": 1, "message": "bad auth"}) @@ -26,7 +26,7 @@ def lndhub_getinfo(): "refresh_token": {"type": "string", "required": True, "excludes": ["login", "password"]}, } ) -def lndhub_auth(): +async def lndhub_auth(): token = ( g.data["token"] if "token" in g.data and g.data["token"] @@ -44,7 +44,7 @@ def lndhub_auth(): "preimage": {"type": "string", "required": False}, } ) -def lndhub_addinvoice(): +async def lndhub_addinvoice(): try: _, pr = create_invoice( wallet_id=g.wallet.id, @@ -76,7 +76,7 @@ def lndhub_addinvoice(): @lndhub_ext.route("/ext/payinvoice", methods=["POST"]) @check_wallet(requires_admin=True) @api_validate_post_request(schema={"invoice": {"type": "string", "required": True}}) -def lndhub_payinvoice(): +async def lndhub_payinvoice(): try: pay_invoice( wallet_id=g.wallet.id, @@ -112,13 +112,13 @@ def lndhub_payinvoice(): @lndhub_ext.route("/ext/balance", methods=["GET"]) @check_wallet() -def lndhub_balance(): +async def lndhub_balance(): return jsonify({"BTC": {"AvailableBalance": g.wallet.balance}}) @lndhub_ext.route("/ext/gettxs", methods=["GET"]) @check_wallet() -def lndhub_gettxs(): +async def lndhub_gettxs(): for payment in g.wallet.get_payments( complete=False, pending=True, outgoing=True, incoming=False, exclude_uncheckable=True ): @@ -146,7 +146,7 @@ def lndhub_gettxs(): @lndhub_ext.route("/ext/getuserinvoices", methods=["GET"]) @check_wallet() -def lndhub_getuserinvoices(): +async def lndhub_getuserinvoices(): delete_expired_invoices() for invoice in g.wallet.get_payments( complete=False, pending=True, outgoing=False, incoming=True, exclude_uncheckable=True @@ -177,26 +177,26 @@ def lndhub_getuserinvoices(): @lndhub_ext.route("/ext/getbtc", methods=["GET"]) @check_wallet() -def lndhub_getbtc(): +async def lndhub_getbtc(): "load an address for incoming onchain btc" return jsonify([]) @lndhub_ext.route("/ext/getpending", methods=["GET"]) @check_wallet() -def lndhub_getpending(): +async def lndhub_getpending(): "pending onchain transactions" return jsonify([]) @lndhub_ext.route("/ext/decodeinvoice", methods=["GET"]) -def lndhub_decodeinvoice(): +async def lndhub_decodeinvoice(): invoice = request.args.get("invoice") inv = bolt11.decode(invoice) return jsonify(decoded_as_lndhub(inv)) @lndhub_ext.route("/ext/checkrouteinvoice", methods=["GET"]) -def lndhub_checkrouteinvoice(): +async def lndhub_checkrouteinvoice(): "not implemented on canonical lndhub" pass diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py index 08ca29c..7a91b6b 100644 --- a/lnbits/extensions/lnticket/__init__.py +++ b/lnbits/extensions/lnticket/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint +from quart import Blueprint lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates") diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html index 4ab829e..59553ab 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/display.html +++ b/lnbits/extensions/lnticket/templates/lnticket/display.html @@ -76,7 +76,6 @@
{% endblock %} {% block scripts %} - -{% assets filters='rjsmin', output='__bundle__/withdraw/index.js', -'withdraw/js/index.js' %} - -{% endassets %} {% endblock %} {% block page %} + +{% endblock %} {% block page %}
@@ -273,7 +270,7 @@ color="deep-purple" :disable=" simpleformDialog.data.wallet == null || - + simpleformDialog.data.max_withdrawable == null || simpleformDialog.data.max_withdrawable < 1 || simpleformDialog.data.uses == null" diff --git a/lnbits/extensions/withdraw/templates/withdraw/print_qr.html b/lnbits/extensions/withdraw/templates/withdraw/print_qr.html index 06744c3..3a47c6b 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/print_qr.html +++ b/lnbits/extensions/withdraw/templates/withdraw/print_qr.html @@ -1,5 +1,4 @@ - -{% block page %} +{% extends "print.html" %} {% block page %}
@@ -50,15 +49,6 @@ } {% endblock %} {% block scripts %} - - - - - - - - - - {% else %} {% assets output='__bundle__/vue.js', - 'vendor/quasar@1.13.2/quasar.ie.polyfills.umd.min.js', - 'vendor/vue@2.6.12/vue.min.js', 'vendor/vue-router@3.4.3/vue-router.min.js', - 'vendor/vuex@3.5.1/vuex.min.js', 'vendor/quasar@1.13.2/quasar.umd.min.js' %} - - {% endassets %} {% endif %} {% assets filters='rjsmin', - output='__bundle__/base.js', 'vendor/axios@0.20.0/axios.min.js', - 'vendor/underscore@1.10.2/underscore.min.js', 'js/base.js', - 'js/components.js' %} - - {% endassets %} {% block scripts %}{% endblock %} + {% block vue_templates %}{% endblock %} + + + + + + + + + + + + + + + {% block scripts %}{% endblock %} diff --git a/lnbits/templates/print.html b/lnbits/templates/print.html index 803e7cf..0af98d9 100644 --- a/lnbits/templates/print.html +++ b/lnbits/templates/print.html @@ -38,13 +38,12 @@ - {% if DEBUG %} - - - {% else %} {% assets output='__bundle__/vue-print.js', - 'vendor/quasar@1.13.2/quasar.ie.polyfills.umd.min.js', - 'vendor/vue@2.6.12/vue.min.js', 'vendor/quasar@1.13.2/quasar.umd.min.js' %} - - {% endassets %} {% endif %} {% block scripts %}{% endblock %} + + + + + + + {% block scripts %}{% endblock %} diff --git a/requirements.txt b/requirements.txt index 52ea1e0..d69fad1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,38 @@ -bech32==1.2.0; python_version >= '3.5' +aiofiles==0.5.0 +bech32==1.2.0 bitstring==3.1.7 +blinker==1.4 brotli==1.0.9 cerberus==1.3.2 certifi==2020.6.20 chardet==3.0.4 -click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +click==7.1.2 ecdsa==0.16.0 environs==8.0.0 -flask-assets==2.0 -flask-compress==1.5.0 -flask-cors==3.0.9 -flask-talisman==0.7.0 -flask==1.1.2 -idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -itsdangerous==1.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -jinja2==2.11.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +h11==0.10.0 +h2==3.2.0 +hpack==3.0.0 +hypercorn==0.10.2 +hyperframe==5.2.0 +idna==2.10 +itsdangerous==1.1.0 +jinja2==2.11.2 lnurl==0.3.5 -markupsafe==1.1.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -marshmallow==3.7.1; python_version >= '3.5' -pydantic==1.6.1; python_version >= '3.6' +markupsafe==1.1.1 +marshmallow==3.7.1 +priority==1.3.0 +pydantic==1.6.1 pyscss==1.3.7 python-dotenv==0.14.0 +quart==0.13.1 +quart-compress==0.2.1 +quart-cors==0.3.0 requests==2.24.0 +secure==0.2.1 shortuuid==1.0.1 -six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -typing-extensions==3.7.4.3; python_version < '3.8' -urllib3==1.25.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' -webassets==2.0 -werkzeug==1.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +six==1.15.0 +toml==0.10.1 +typing-extensions==3.7.4.3 +urllib3==1.25.10 +werkzeug==1.0.1 +wsproto==0.15.0 From 42c6620a0742cfa0c8a52306f7968536a7451186 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 14 Sep 2020 23:28:10 -0300 Subject: [PATCH 2/9] quart run settings and hypercorn on docker/Procfile. --- .env.example | 8 ++++++-- Dockerfile | 2 +- Procfile | 2 +- lnbits/__main__.py | 2 +- lnbits/settings.py | 5 +++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index a43bfca..6c9f41c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,9 @@ -FLASK_APP=lnbits.app -FLASK_ENV=development +QUART_APP=lnbits.app +QUART_ENV=development +QUART_DEBUG=1 + +HOST=127.0.0.1 +PORT=5000 LNBITS_SITE_TITLE=LNbits LNBITS_ALLOWED_USERS="" diff --git a/Dockerfile b/Dockerfile index 9e53a61..f959cbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.7-slim WORKDIR /app COPY requirements.txt /app/ RUN pip install --no-cache-dir -q -r requirements.txt -RUN pip install --no-cache-dir -q gunicorn gevent +RUN pip install --no-cache-dir -q hypercorn COPY . /app EXPOSE 5000 diff --git a/Procfile b/Procfile index 3d7b459..0b1764f 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: gunicorn -b :5000 lnbits:app -k gevent +web: hypercorn --bind 0.0.0.0:5000 lnbits:app diff --git a/lnbits/__main__.py b/lnbits/__main__.py index 31a4de1..2fa0c72 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -5,4 +5,4 @@ from .commands import migrate_databases migrate_databases() app = create_app() -app.run() +app.run(host=app.config["HOST"], port=app.config["PORT"]) diff --git a/lnbits/settings.py b/lnbits/settings.py index 905003e..b33356a 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -11,6 +11,11 @@ env.read_env() wallets_module = importlib.import_module("lnbits.wallets") wallet_class = getattr(wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet")) +ENV = env.str("QUART_ENV", default="production") +DEBUG = env.bool("QUART_DEBUG") or ENV == "development" +HOST = env.str("HOST", default="127.0.0.1") +PORT = env.int("PORT", default=5000) + LNBITS_PATH = path.dirname(path.realpath(__file__)) LNBITS_DATA_FOLDER = env.str("LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data")) LNBITS_ALLOWED_USERS: List[str] = env.list("LNBITS_ALLOWED_USERS", default=[], subcast=str) From 3a0762ff82a27e553125fdd55a466b63089ba775 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 14 Sep 2020 23:55:47 -0300 Subject: [PATCH 3/9] proxy fix (x-forwarded-...). --- lnbits/app.py | 2 + lnbits/proxy_fix.py | 100 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 lnbits/proxy_fix.py diff --git a/lnbits/app.py b/lnbits/app.py index 31edcc1..706282e 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -9,6 +9,7 @@ from .commands import db_migrate from .core import core_app from .db import open_db from .helpers import get_valid_extensions +from .proxy_fix import ProxyFix secure_headers = SecureHeaders(hsts=False) @@ -22,6 +23,7 @@ def create_app(config_object="lnbits.settings") -> Quart: cors(app) Compress(app) + ProxyFix(app) register_blueprints(app) register_filters(app) diff --git a/lnbits/proxy_fix.py b/lnbits/proxy_fix.py new file mode 100644 index 0000000..b3751d8 --- /dev/null +++ b/lnbits/proxy_fix.py @@ -0,0 +1,100 @@ +from typing import Optional, List +from urllib.request import parse_http_list as _parse_list_header + +from quart import request + + +class ProxyFix: + def __init__(self, app=None, x_for: int = 1, x_proto: int = 1, x_host: int = 0, x_port: int = 0, x_prefix: int = 0): + self.app = app + self.x_for = x_for + self.x_proto = x_proto + self.x_host = x_host + self.x_port = x_port + self.x_prefix = x_prefix + + if app: + self.init_app(app) + + def init_app(self, app): + @app.before_request + async def before_request(): + x_for = self._get_real_value(self.x_for, request.headers.get("X-Forwarded-For")) + if x_for: + request.headers["Remote-Addr"] = x_for + + x_proto = self._get_real_value(self.x_proto, request.headers.get("X-Forwarded-Proto")) + if x_proto: + request.scheme = x_proto + + x_host = self._get_real_value(self.x_host, request.headers.get("X-Forwarded-Host")) + if x_host: + request.headers["host"] = x_host.lower() + parts = x_host.split(":", 1) + # environ["SERVER_NAME"] = parts[0] + # if len(parts) == 2: + # environ["SERVER_PORT"] = parts[1] + + x_port = self._get_real_value(self.x_port, request.headers.get("X-Forwarded-Port")) + if x_port: + host = request.host + if host: + parts = host.split(":", 1) + host = parts[0] if len(parts) == 2 else host + request.headers["host"] = f"{host}:{x_port}" + # environ["SERVER_PORT"] = x_port + + def _get_real_value(self, trusted: int, value: Optional[str]) -> Optional[str]: + """Get the real value from a list header based on the configured + number of trusted proxies. + :param trusted: Number of values to trust in the header. + :param value: Comma separated list header value to parse. + :return: The real value, or ``None`` if there are fewer values + than the number of trusted proxies. + .. versionchanged:: 1.0 + Renamed from ``_get_trusted_comma``. + .. versionadded:: 0.15 + """ + if not (trusted and value): + return None + + values = self.parse_list_header(value) + if len(values) >= trusted: + return values[-trusted] + + return None + + def parse_list_header(self, value: str) -> List[str]: + result = [] + for item in _parse_list_header(value): + if item[:1] == item[-1:] == '"': + item = self.unquote_header_value(item[1:-1]) + result.append(item) + return result + + def unquote_header_value(self, value: str, is_filename: bool = False) -> str: + r"""Unquotes a header value. (Reversal of :func:`quote_header_value`). + This does not use the real unquoting but what browsers are actually + using for quoting. + .. versionadded:: 0.5 + :param value: the header value to unquote. + :param is_filename: The value represents a filename or path. + """ + if value and value[0] == value[-1] == '"': + # this is not the real unquoting, but fixing this so that the + # RFC is met will result in bugs with internet explorer and + # probably some other browsers as well. IE for example is + # uploading files with "C:\foo\bar.txt" as filename + value = value[1:-1] + + # if this is a filename and the starting characters look like + # a UNC path, then just return the value without quotes. Using the + # replace sequence below on a UNC path has the effect of turning + # the leading double slash into a single slash and then + # _fix_ie_filename() doesn't work correctly. See #458. + if not is_filename or value[:2] != "\\\\": + return value.replace("\\\\", "\\").replace('\\"', '"') + return value + + +# host, request.root_path, subdomain, request.scheme, request.method, request.path, request.query_string.decode(), From 6928f431a759c49a990a6a8cc66466cec66c01dc Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 15 Sep 2020 15:54:05 -0300 Subject: [PATCH 4/9] poor man's flask-assets. --- .gitignore | 1 + Makefile | 5 +- lnbits/__main__.py | 5 +- lnbits/app.py | 18 +- lnbits/commands.py | 31 +- lnbits/helpers.py | 52 +++ lnbits/settings.py | 2 +- lnbits/static/css/.gitignore | 1 + lnbits/static/css/base.css | 1 - lnbits/static/vendor/bolt11/decoder.js | 507 +++++++++++++++---------- lnbits/static/vendor/bolt11/utils.js | 96 ----- lnbits/templates/base.html | 31 +- lnbits/templates/print.html | 18 +- 13 files changed, 430 insertions(+), 338 deletions(-) create mode 100644 lnbits/static/css/.gitignore delete mode 100644 lnbits/static/css/base.css delete mode 100644 lnbits/static/vendor/bolt11/utils.js diff --git a/.gitignore b/.gitignore index 08b009f..ca3fcd0 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ venv __bundle__ node_modules +lnbits/static/bundle.* diff --git a/Makefile b/Makefile index a48fa1a..f80747a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: format check lnbits/static/css/base.css requirements.txt +all: format check requirements.txt format: prettier black @@ -19,8 +19,5 @@ checkprettier: $(shell find lnbits -name "*.js" -name ".html") checkblack: $(shell find lnbits -name "*.py") ./venv/bin/black --check lnbits -lnbits/static/css/base.css: lnbits/static/scss/base.scss - ./venv/bin/pyscss -o lnbits/static/css/base.css lnbits/static/scss/base.scss - requirements.txt: Pipfile.lock cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt diff --git a/lnbits/__main__.py b/lnbits/__main__.py index 2fa0c72..6932fba 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -1,8 +1,9 @@ from .app import create_app -from .commands import migrate_databases - +from .commands import migrate_databases, transpile_scss, bundle_vendored migrate_databases() +transpile_scss() +bundle_vendored() app = create_app() app.run(host=app.config["HOST"], port=app.config["PORT"]) diff --git a/lnbits/app.py b/lnbits/app.py index 706282e..8bcc5be 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -8,7 +8,7 @@ from secure import SecureHeaders # type: ignore from .commands import db_migrate from .core import core_app from .db import open_db -from .helpers import get_valid_extensions +from .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored from .proxy_fix import ProxyFix secure_headers = SecureHeaders(hsts=False) @@ -25,6 +25,7 @@ def create_app(config_object="lnbits.settings") -> Quart: Compress(app) ProxyFix(app) + register_assets(app) register_blueprints(app) register_filters(app) register_commands(app) @@ -50,10 +51,23 @@ def register_commands(app): app.cli.add_command(db_migrate) +def register_assets(app): + """Serve each vendored asset separately or a bundle.""" + + @app.before_request + async def vendored_assets_variable(): + if app.config["DEBUG"]: + g.VENDORED_JS = map(url_for_vendored, get_js_vendored()) + g.VENDORED_CSS = map(url_for_vendored, get_css_vendored()) + else: + g.VENDORED_JS = ["/static/bundle.js"] + g.VENDORED_CSS = ["/static/bundle.css"] + + def register_filters(app): """Jinja filters.""" - app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions() app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"] + app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions() def register_request_hooks(app): diff --git a/lnbits/commands.py b/lnbits/commands.py index 60170a9..0cc5eff 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -1,11 +1,15 @@ import click import importlib import re +import os import sqlite3 +from scss.compiler import compile_string + from .core import migrations as core_migrations from .db import open_db, open_ext_db -from .helpers import get_valid_extensions +from .helpers import get_valid_extensions, get_css_vendored, get_js_vendored, url_for_vendored +from .settings import LNBITS_PATH @click.command("migrate") @@ -13,6 +17,31 @@ def db_migrate(): migrate_databases() +@click.command("assets") +def handle_assets(): + transpile_scss() + bundle_vendored() + + +def transpile_scss(): + with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss: + with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css: + css.write(compile_string(scss.read())) + + +def bundle_vendored(): + for getfiles, outputpath in [ + (get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")), + (get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")), + ]: + output = "" + for path in getfiles(): + with open(path) as f: + output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n" + with open(outputpath, "w") as f: + f.write(output) + + def migrate_databases(): """Creates the necessary databases if they don't exist already; or migrates them.""" diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 63cecff..dec7cb6 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -1,5 +1,6 @@ import json import os +import glob import shortuuid # type: ignore from typing import List, NamedTuple, Optional @@ -54,3 +55,54 @@ def get_valid_extensions() -> List[Extension]: def urlsafe_short_hash() -> str: return shortuuid.uuid() + + +def get_js_vendored(prefer_minified: bool = False) -> List[str]: + paths = get_vendored(".js", prefer_minified) + + def sorter(key: str): + if "moment@" in key: + return 1 + if "vue@" in key: + return 2 + if "vue-router@" in key: + return 3 + if "polyfills" in key: + return 4 + return 9 + + return sorted(paths, key=sorter) + + +def get_css_vendored(prefer_minified: bool = False) -> List[str]: + return get_vendored(".css", prefer_minified) + + +def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: + paths: List[str] = [] + for path in glob.glob(os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True): + if path.endswith(".min" + ext): + # path is minified + unminified = path.replace(".min" + ext, ext) + if prefer_minified: + paths.append(path) + if unminified in paths: + paths.remove(unminified) + elif unminified not in paths: + paths.append(path) + + elif path.endswith(ext): + # path is not minified + minified = path.replace(ext, ".min" + ext) + if not prefer_minified: + paths.append(path) + if minified in paths: + paths.remove(minified) + elif minified not in paths: + paths.append(path) + + return paths + + +def url_for_vendored(abspath: str) -> str: + return "/" + os.path.relpath(abspath, LNBITS_PATH) diff --git a/lnbits/settings.py b/lnbits/settings.py index b33356a..983b79e 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -12,7 +12,7 @@ wallets_module = importlib.import_module("lnbits.wallets") wallet_class = getattr(wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet")) ENV = env.str("QUART_ENV", default="production") -DEBUG = env.bool("QUART_DEBUG") or ENV == "development" +DEBUG = env.bool("QUART_DEBUG", default=False) or ENV == "development" HOST = env.str("HOST", default="127.0.0.1") PORT = env.int("PORT", default=5000) diff --git a/lnbits/static/css/.gitignore b/lnbits/static/css/.gitignore new file mode 100644 index 0000000..d692bf8 --- /dev/null +++ b/lnbits/static/css/.gitignore @@ -0,0 +1 @@ +base.css diff --git a/lnbits/static/css/base.css b/lnbits/static/css/base.css deleted file mode 100644 index 457a081..0000000 --- a/lnbits/static/css/base.css +++ /dev/null @@ -1 +0,0 @@ -[v-cloak]{display:none}.bg-lnbits-dark{background-color:#1f2234}body.body--dark,body.body--dark .q-drawer--dark,body.body--dark .q-menu--dark{background:#1f2234}body.body--dark .q-card--dark{background:#333646}body.body--dark .q-table--dark{background:transparent}body.body--light,body.body--light .q-drawer{background:#f5f5f5}body.body--dark .q-field--error .text-negative,body.body--dark .q-field--error .q-field__messages{color:#ff0 !important}.lnbits-drawer__q-list .q-item{padding-top:5px !important;padding-bottom:5px !important;border-top-right-radius:3px;border-bottom-right-radius:3px}.lnbits-drawer__q-list .q-item.q-item--active{color:inherit;font-weight:bold}.lnbits__dialog-card{width:500px}.q-table--dense th:first-child,.q-table--dense td:first-child,.q-table--dense .q-table__bottom{padding-left:6px !important}.q-table--dense th:last-child,.q-table--dense td:last-child,.q-table--dense .q-table__bottom{padding-right:6px !important}a.inherit{color:inherit;text-decoration:none}video{border-radius:3px}@font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(../fonts/material-icons-v50.woff2) format('woff2')}.material-icons{font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-moz-font-feature-settings:'liga';-moz-osx-font-smoothing:grayscale} diff --git a/lnbits/static/vendor/bolt11/decoder.js b/lnbits/static/vendor/bolt11/decoder.js index 88701ec..7b73260 100644 --- a/lnbits/static/vendor/bolt11/decoder.js +++ b/lnbits/static/vendor/bolt11/decoder.js @@ -4,233 +4,344 @@ //TODO - A reader MUST use the n field to validate the signature instead of performing signature recovery if a valid n field is provided. function decode(paymentRequest) { - let input = paymentRequest.toLowerCase(); - let splitPosition = input.lastIndexOf('1'); - let humanReadablePart = input.substring(0, splitPosition); - let data = input.substring(splitPosition + 1, input.length - 6); - let checksum = input.substring(input.length - 6, input.length); - if (!verify_checksum(humanReadablePart, bech32ToFiveBitArray(data + checksum))) { - throw 'Malformed request: checksum is incorrect'; // A reader MUST fail if the checksum is incorrect. - } - return { - 'human_readable_part': decodeHumanReadablePart(humanReadablePart), - 'data': decodeData(data, humanReadablePart), - 'checksum': checksum - } + let input = paymentRequest.toLowerCase() + let splitPosition = input.lastIndexOf('1') + let humanReadablePart = input.substring(0, splitPosition) + let data = input.substring(splitPosition + 1, input.length - 6) + let checksum = input.substring(input.length - 6, input.length) + if ( + !verify_checksum(humanReadablePart, bech32ToFiveBitArray(data + checksum)) + ) { + throw 'Malformed request: checksum is incorrect' // A reader MUST fail if the checksum is incorrect. + } + return { + human_readable_part: decodeHumanReadablePart(humanReadablePart), + data: decodeData(data, humanReadablePart), + checksum: checksum + } } function decodeHumanReadablePart(humanReadablePart) { - let prefixes = ['lnbc', 'lntb', 'lnbcrt', 'lnsb']; - let prefix; - prefixes.forEach(value => { - if (humanReadablePart.substring(0, value.length) === value) { - prefix = value; - } - }); - if (prefix == null) throw 'Malformed request: unknown prefix'; // A reader MUST fail if it does not understand the prefix. - let amount = decodeAmount(humanReadablePart.substring(prefix.length, humanReadablePart.length)); - return { - 'prefix': prefix, - 'amount': amount + let prefixes = ['lnbc', 'lntb', 'lnbcrt', 'lnsb'] + let prefix + prefixes.forEach(value => { + if (humanReadablePart.substring(0, value.length) === value) { + prefix = value } + }) + if (prefix == null) throw 'Malformed request: unknown prefix' // A reader MUST fail if it does not understand the prefix. + let amount = decodeAmount( + humanReadablePart.substring(prefix.length, humanReadablePart.length) + ) + return { + prefix: prefix, + amount: amount + } } function decodeData(data, humanReadablePart) { - let date32 = data.substring(0, 7); - let dateEpoch = bech32ToInt(date32); - let signature = data.substring(data.length - 104, data.length); - let tagData = data.substring(7, data.length - 104); - let decodedTags = decodeTags(tagData); - let value = bech32ToFiveBitArray(date32 + tagData); - value = fiveBitArrayTo8BitArray(value, true); - value = textToHexString(humanReadablePart).concat(byteArrayToHexString(value)); - return { - 'time_stamp': dateEpoch, - 'tags': decodedTags, - 'signature': decodeSignature(signature), - 'signing_data': value - } + let date32 = data.substring(0, 7) + let dateEpoch = bech32ToInt(date32) + let signature = data.substring(data.length - 104, data.length) + let tagData = data.substring(7, data.length - 104) + let decodedTags = decodeTags(tagData) + let value = bech32ToFiveBitArray(date32 + tagData) + value = fiveBitArrayTo8BitArray(value, true) + value = textToHexString(humanReadablePart).concat(byteArrayToHexString(value)) + return { + time_stamp: dateEpoch, + tags: decodedTags, + signature: decodeSignature(signature), + signing_data: value + } } function decodeSignature(signature) { - let data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(signature)); - let recoveryFlag = data[data.length - 1]; - let r = byteArrayToHexString(data.slice(0, 32)); - let s = byteArrayToHexString(data.slice(32, data.length - 1)); - return { - 'r': r, - 's': s, - 'recovery_flag': recoveryFlag - } + let data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(signature)) + let recoveryFlag = data[data.length - 1] + let r = byteArrayToHexString(data.slice(0, 32)) + let s = byteArrayToHexString(data.slice(32, data.length - 1)) + return { + r: r, + s: s, + recovery_flag: recoveryFlag + } } function decodeAmount(str) { - let multiplier = str.charAt(str.length - 1); - let amount = str.substring(0, str.length - 1); - if (amount.substring(0, 1) === '0') { - throw 'Malformed request: amount cannot contain leading zeros'; - } - amount = Number(amount); - if (amount < 0 || !Number.isInteger(amount)) { - throw 'Malformed request: amount must be a positive decimal integer'; // A reader SHOULD fail if amount contains a non-digit - } + let multiplier = str.charAt(str.length - 1) + let amount = str.substring(0, str.length - 1) + if (amount.substring(0, 1) === '0') { + throw 'Malformed request: amount cannot contain leading zeros' + } + amount = Number(amount) + if (amount < 0 || !Number.isInteger(amount)) { + throw 'Malformed request: amount must be a positive decimal integer' // A reader SHOULD fail if amount contains a non-digit + } - switch (multiplier) { - case '': - return 'Any amount'; // A reader SHOULD indicate if amount is unspecified - case 'p': - return amount / 10; - case 'n': - return amount * 100; - case 'u': - return amount * 100000; - case 'm': - return amount * 100000000; - default: - // A reader SHOULD fail if amount is followed by anything except a defined multiplier. - throw 'Malformed request: undefined amount multiplier'; - } + switch (multiplier) { + case '': + return 'Any amount' // A reader SHOULD indicate if amount is unspecified + case 'p': + return amount / 10 + case 'n': + return amount * 100 + case 'u': + return amount * 100000 + case 'm': + return amount * 100000000 + default: + // A reader SHOULD fail if amount is followed by anything except a defined multiplier. + throw 'Malformed request: undefined amount multiplier' + } } function decodeTags(tagData) { - let tags = extractTags(tagData); - let decodedTags = []; - tags.forEach(value => decodedTags.push(decodeTag(value.type, value.length, value.data))); - return decodedTags; + let tags = extractTags(tagData) + let decodedTags = [] + tags.forEach(value => + decodedTags.push(decodeTag(value.type, value.length, value.data)) + ) + return decodedTags } function extractTags(str) { - let tags = []; - while (str.length > 0) { - let type = str.charAt(0); - let dataLength = bech32ToInt(str.substring(1, 3)); - let data = str.substring(3, dataLength + 3); - tags.push({ - 'type': type, - 'length': dataLength, - 'data': data - }); - str = str.substring(3 + dataLength, str.length); - } - return tags; + let tags = [] + while (str.length > 0) { + let type = str.charAt(0) + let dataLength = bech32ToInt(str.substring(1, 3)) + let data = str.substring(3, dataLength + 3) + tags.push({ + type: type, + length: dataLength, + data: data + }) + str = str.substring(3 + dataLength, str.length) + } + return tags } function decodeTag(type, length, data) { - switch (type) { - case 'p': - if (length !== 52) break; // A reader MUST skip over a 'p' field that does not have data_length 52 - return { - 'type': type, - 'length': length, - 'description': 'payment_hash', - 'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))) - }; - case 'd': - return { - 'type': type, - 'length': length, - 'description': 'description', - 'value': bech32ToUTF8String(data) - }; - case 'n': - if (length !== 53) break; // A reader MUST skip over a 'n' field that does not have data_length 53 - return { - 'type': type, - 'length': length, - 'description': 'payee_public_key', - 'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))) - }; - case 'h': - if (length !== 52) break; // A reader MUST skip over a 'h' field that does not have data_length 52 - return { - 'type': type, - 'length': length, - 'description': 'description_hash', - 'value': data - }; - case 'x': - return { - 'type': type, - 'length': length, - 'description': 'expiry', - 'value': bech32ToInt(data) - }; - case 'c': - return { - 'type': type, - 'length': length, - 'description': 'min_final_cltv_expiry', - 'value': bech32ToInt(data) - }; - case 'f': - let version = bech32ToFiveBitArray(data.charAt(0))[0]; - if (version < 0 || version > 18) break; // a reader MUST skip over an f field with unknown version. - data = data.substring(1, data.length); - return { - 'type': type, - 'length': length, - 'description': 'fallback_address', - 'value': { - 'version': version, - 'fallback_address': data - } - }; - case 'r': - data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)); - let pubkey = data.slice(0, 33); - let shortChannelId = data.slice(33, 41); - let feeBaseMsat = data.slice(41, 45); - let feeProportionalMillionths = data.slice(45, 49); - let cltvExpiryDelta = data.slice(49, 51); - return { - 'type': type, - 'length': length, - 'description': 'routing_information', - 'value': { - 'public_key': byteArrayToHexString(pubkey), - 'short_channel_id': byteArrayToHexString(shortChannelId), - 'fee_base_msat': byteArrayToInt(feeBaseMsat), - 'fee_proportional_millionths': byteArrayToInt(feeProportionalMillionths), - 'cltv_expiry_delta': byteArrayToInt(cltvExpiryDelta) - } - }; - default: - // reader MUST skip over unknown fields - } + switch (type) { + case 'p': + if (length !== 52) break // A reader MUST skip over a 'p' field that does not have data_length 52 + return { + type: type, + length: length, + description: 'payment_hash', + value: byteArrayToHexString( + fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)) + ) + } + case 'd': + return { + type: type, + length: length, + description: 'description', + value: bech32ToUTF8String(data) + } + case 'n': + if (length !== 53) break // A reader MUST skip over a 'n' field that does not have data_length 53 + return { + type: type, + length: length, + description: 'payee_public_key', + value: byteArrayToHexString( + fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)) + ) + } + case 'h': + if (length !== 52) break // A reader MUST skip over a 'h' field that does not have data_length 52 + return { + type: type, + length: length, + description: 'description_hash', + value: data + } + case 'x': + return { + type: type, + length: length, + description: 'expiry', + value: bech32ToInt(data) + } + case 'c': + return { + type: type, + length: length, + description: 'min_final_cltv_expiry', + value: bech32ToInt(data) + } + case 'f': + let version = bech32ToFiveBitArray(data.charAt(0))[0] + if (version < 0 || version > 18) break // a reader MUST skip over an f field with unknown version. + data = data.substring(1, data.length) + return { + type: type, + length: length, + description: 'fallback_address', + value: { + version: version, + fallback_address: data + } + } + case 'r': + data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)) + let pubkey = data.slice(0, 33) + let shortChannelId = data.slice(33, 41) + let feeBaseMsat = data.slice(41, 45) + let feeProportionalMillionths = data.slice(45, 49) + let cltvExpiryDelta = data.slice(49, 51) + return { + type: type, + length: length, + description: 'routing_information', + value: { + public_key: byteArrayToHexString(pubkey), + short_channel_id: byteArrayToHexString(shortChannelId), + fee_base_msat: byteArrayToInt(feeBaseMsat), + fee_proportional_millionths: byteArrayToInt( + feeProportionalMillionths + ), + cltv_expiry_delta: byteArrayToInt(cltvExpiryDelta) + } + } + default: + // reader MUST skip over unknown fields + } } function polymod(values) { - let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; - let chk = 1; - values.forEach((value) => { - let b = (chk >> 25); - chk = (chk & 0x1ffffff) << 5 ^ value; - for (let i = 0; i < 5; i++) { - if (((b >> i) & 1) === 1) { - chk ^= GEN[i]; - } else { - chk ^= 0; - } - } - }); - return chk; + let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + let chk = 1 + values.forEach(value => { + let b = chk >> 25 + chk = ((chk & 0x1ffffff) << 5) ^ value + for (let i = 0; i < 5; i++) { + if (((b >> i) & 1) === 1) { + chk ^= GEN[i] + } else { + chk ^= 0 + } + } + }) + return chk } function expand(str) { - let array = []; - for (let i = 0; i < str.length; i++) { - array.push(str.charCodeAt(i) >> 5); - } - array.push(0); - for (let i = 0; i < str.length; i++) { - array.push(str.charCodeAt(i) & 31); - } - return array; + let array = [] + for (let i = 0; i < str.length; i++) { + array.push(str.charCodeAt(i) >> 5) + } + array.push(0) + for (let i = 0; i < str.length; i++) { + array.push(str.charCodeAt(i) & 31) + } + return array } function verify_checksum(hrp, data) { - hrp = expand(hrp); - let all = hrp.concat(data); - let bool = polymod(all); - return bool === 1; -} \ No newline at end of file + hrp = expand(hrp) + let all = hrp.concat(data) + let bool = polymod(all) + return bool === 1 +} + +const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + +function byteArrayToInt(byteArray) { + let value = 0 + for (let i = 0; i < byteArray.length; ++i) { + value = (value << 8) + byteArray[i] + } + return value +} + +function bech32ToInt(str) { + let sum = 0 + for (let i = 0; i < str.length; i++) { + sum = sum * 32 + sum = sum + bech32CharValues.indexOf(str.charAt(i)) + } + return sum +} + +function bech32ToFiveBitArray(str) { + let array = [] + for (let i = 0; i < str.length; i++) { + array.push(bech32CharValues.indexOf(str.charAt(i))) + } + return array +} + +function fiveBitArrayTo8BitArray(int5Array, includeOverflow) { + let count = 0 + let buffer = 0 + let byteArray = [] + int5Array.forEach(value => { + buffer = (buffer << 5) + value + count += 5 + if (count >= 8) { + byteArray.push((buffer >> (count - 8)) & 255) + count -= 8 + } + }) + if (includeOverflow && count > 0) { + byteArray.push((buffer << (8 - count)) & 255) + } + return byteArray +} + +function bech32ToUTF8String(str) { + let int5Array = bech32ToFiveBitArray(str) + let byteArray = fiveBitArrayTo8BitArray(int5Array) + + let utf8String = '' + for (let i = 0; i < byteArray.length; i++) { + utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2) + } + return decodeURIComponent(utf8String) +} + +function byteArrayToHexString(byteArray) { + return Array.prototype.map + .call(byteArray, function (byte) { + return ('0' + (byte & 0xff).toString(16)).slice(-2) + }) + .join('') +} + +function textToHexString(text) { + let hexString = '' + for (let i = 0; i < text.length; i++) { + hexString += text.charCodeAt(i).toString(16) + } + return hexString +} + +function epochToDate(int) { + let date = new Date(int * 1000) + return date.toUTCString() +} + +function isEmptyOrSpaces(str) { + return str === null || str.match(/^ *$/) !== null +} + +function toFixed(x) { + if (Math.abs(x) < 1.0) { + var e = parseInt(x.toString().split('e-')[1]) + if (e) { + x *= Math.pow(10, e - 1) + x = '0.' + new Array(e).join('0') + x.toString().substring(2) + } + } else { + var e = parseInt(x.toString().split('+')[1]) + if (e > 20) { + e -= 20 + x /= Math.pow(10, e) + x += new Array(e + 1).join('0') + } + } + return x +} diff --git a/lnbits/static/vendor/bolt11/utils.js b/lnbits/static/vendor/bolt11/utils.js deleted file mode 100644 index f2b75bc..0000000 --- a/lnbits/static/vendor/bolt11/utils.js +++ /dev/null @@ -1,96 +0,0 @@ -const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; - -function byteArrayToInt(byteArray) { - let value = 0; - for (let i = 0; i < byteArray.length; ++i) { - value = (value << 8) + byteArray[i]; - } - return value; -} - -function bech32ToInt(str) { - let sum = 0; - for (let i = 0; i < str.length; i++) { - sum = sum * 32; - sum = sum + bech32CharValues.indexOf(str.charAt(i)); - } - return sum; -} - -function bech32ToFiveBitArray(str) { - let array = []; - for (let i = 0; i < str.length; i++) { - array.push(bech32CharValues.indexOf(str.charAt(i))); - } - return array; -} - -function fiveBitArrayTo8BitArray(int5Array, includeOverflow) { - let count = 0; - let buffer = 0; - let byteArray = []; - int5Array.forEach((value) => { - buffer = (buffer << 5) + value; - count += 5; - if (count >= 8) { - byteArray.push(buffer >> (count - 8) & 255); - count -= 8; - } - }); - if (includeOverflow && count > 0) { - byteArray.push(buffer << (8 - count) & 255); - } - return byteArray; -} - -function bech32ToUTF8String(str) { - let int5Array = bech32ToFiveBitArray(str); - let byteArray = fiveBitArrayTo8BitArray(int5Array); - - let utf8String = ''; - for (let i = 0; i < byteArray.length; i++) { - utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2); - } - return decodeURIComponent(utf8String); -} - -function byteArrayToHexString(byteArray) { - return Array.prototype.map.call(byteArray, function (byte) { - return ('0' + (byte & 0xFF).toString(16)).slice(-2); - }).join(''); -} - -function textToHexString(text) { - let hexString = ''; - for (let i = 0; i < text.length; i++) { - hexString += text.charCodeAt(i).toString(16); - } - return hexString; -} - -function epochToDate(int) { - let date = new Date(int * 1000); - return date.toUTCString(); -} - -function isEmptyOrSpaces(str){ - return str === null || str.match(/^ *$/) !== null; -} - -function toFixed(x) { - if (Math.abs(x) < 1.0) { - var e = parseInt(x.toString().split('e-')[1]); - if (e) { - x *= Math.pow(10,e-1); - x = '0.' + (new Array(e)).join('0') + x.toString().substring(2); - } - } else { - var e = parseInt(x.toString().split('+')[1]); - if (e > 20) { - e -= 20; - x /= Math.pow(10,e); - x += (new Array(e+1)).join('0'); - } - } - return x; -} \ No newline at end of file diff --git a/lnbits/templates/base.html b/lnbits/templates/base.html index b0f2a62..7dce34e 100644 --- a/lnbits/templates/base.html +++ b/lnbits/templates/base.html @@ -2,16 +2,10 @@ - - + {% for url in g.VENDORED_CSS %} + + {% endfor %} + {% block styles %}{% endblock %} @@ -113,18 +107,11 @@ </q-layout> {% block vue_templates %}{% endblock %} - <script src="/static/vendor/vue@2.6.12/vue.js"></script> - <script src="/static/vendor/vuex@3.5.1/vuex.js"></script> - <script src="/static/vendor/vue-router@3.4.3/vue-router.js"></script> - <script src="/static/vendor/quasar@1.13.2/quasar.umd.js"></script> - <script src="/static/vendor/axios@0.20.0/axios.min.js"></script> - <script src="/static/vendor/underscore@1.10.2/underscore.min.js"></script> - <script src="/static/vendor/vue-qrcode@1.0.2/vue-qrcode.min.js"></script> - <script src="/static/vendor/moment@2.27.0/moment.min.js"></script> - <script src="/static/vendor/chart.js@2.9.3/chart.min.js"></script> - <script src="/static/vendor/bolt11/utils.js"></script> - <script src="/static/vendor/bolt11/decoder.js"></script> - <script src="/static/vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js"></script> + <!----> + {% for url in g.VENDORED_JS %} + <script src="{{ url }}"></script> + {% endfor %} + <!----> <script src="/static/js/base.js"></script> <script src="/static/js/components.js"></script> {% block scripts %}{% endblock %} diff --git a/lnbits/templates/print.html b/lnbits/templates/print.html index 0af98d9..b62ee15 100644 --- a/lnbits/templates/print.html +++ b/lnbits/templates/print.html @@ -2,11 +2,9 @@ <html lang="en"> <head> - <link - rel="stylesheet" - href="//fonts.googleapis.com/css?family=Material+Icons" - type="text/css" - /> + {% for url in g.VENDORED_CSS %} + <link rel="stylesheet" type="text/css" href="{{ url }}" /> + {% endfor %} <style> @page { size: A4 portrait; @@ -38,12 +36,10 @@ </q-page-container> </q-layout> - <script src="/static/vendor/quasar@1.13.2/quasar.ie.polyfills.umd.min.js"></script> - <script src="/static/vendor/vue@2.6.12/vue.min.js"></script> - <script src="/static/vendor/vuex@3.5.1/vuex.js"></script> - <script src="/static/vendor/vue-router@3.4.3/vue-router.js"></script> - <script src="/static/vendor/quasar@1.13.2/quasar.umd.min.js"></script> - <script src="/static/vendor/vue-qrcode@1.0.2/vue-qrcode.min.js"></script> + {% for url in g.VENDORED_JS %} + <script src="{{ url }}"></script> + {% endfor %} + <!----> {% block scripts %}{% endblock %} </body> </html> From 5513b34a66671f6d20d5db36be81c31bfb0f0feb Mon Sep 17 00:00:00 2001 From: fiatjaf <fiatjaf@alhur.es> Date: Tue, 15 Sep 2020 17:19:11 -0300 Subject: [PATCH 5/9] change 1 to true on env vars because that is supported now. --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 6c9f41c..b6e88e4 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ QUART_APP=lnbits.app QUART_ENV=development -QUART_DEBUG=1 +QUART_DEBUG=true HOST=127.0.0.1 PORT=5000 @@ -10,7 +10,7 @@ LNBITS_ALLOWED_USERS="" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DATA_FOLDER="/your_custom_data_folder" #IMPORTANT! i.e. "/home/satoshi/lnbits/lnbits/data" LNBITS_DISABLED_EXTENSIONS="amilk" -LNBITS_FORCE_HTTPS=1 +LNBITS_FORCE_HTTPS=true LNBITS_SERVICE_FEE="0.0" # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), LndRestWallet, CLightningWallet, LnbitsWallet From 2b38207100fe1a5d4fe23b837b78aeb5453cbe0e Mon Sep 17 00:00:00 2001 From: fiatjaf <fiatjaf@alhur.es> Date: Tue, 15 Sep 2020 17:19:34 -0300 Subject: [PATCH 6/9] mimic x_proto, x_host params from old ProxyFix. --- lnbits/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/app.py b/lnbits/app.py index 8bcc5be..8bda0e8 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -23,7 +23,7 @@ def create_app(config_object="lnbits.settings") -> Quart: cors(app) Compress(app) - ProxyFix(app) + ProxyFix(app, x_proto=1, x_host=1) register_assets(app) register_blueprints(app) From 75047c5bc350fd3271ee7867c5bfc795a07cecef Mon Sep 17 00:00:00 2001 From: fiatjaf <fiatjaf@alhur.es> Date: Wed, 16 Sep 2020 20:21:42 -0300 Subject: [PATCH 7/9] fix static extensions js url. --- lnbits/core/templates/core/extensions.html | 2 +- lnbits/extensions/withdraw/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lnbits/core/templates/core/extensions.html b/lnbits/core/templates/core/extensions.html index 8f7e92b..97fa893 100644 --- a/lnbits/core/templates/core/extensions.html +++ b/lnbits/core/templates/core/extensions.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block scripts %} {{ window_vars(user) }} -<script src="/static/core/js/extensions.js"></script> +<script src="/core/static/js/extensions.js"></script> {% endblock %} {% block page %} <div class="row q-col-gutter-md"> <div diff --git a/lnbits/extensions/withdraw/__init__.py b/lnbits/extensions/withdraw/__init__.py index 772cfa2..ce5970e 100644 --- a/lnbits/extensions/withdraw/__init__.py +++ b/lnbits/extensions/withdraw/__init__.py @@ -1,9 +1,7 @@ from quart import Blueprint -withdraw_ext: Blueprint = Blueprint( - "withdraw", __name__, static_folder="static", template_folder="templates", static_url_path="/static" -) +withdraw_ext: Blueprint = Blueprint("withdraw", __name__, static_folder="static", template_folder="templates") from .views_api import * # noqa From ef0bdf8363772fbf18de760388bdfe5b873abefe Mon Sep 17 00:00:00 2001 From: fiatjaf <fiatjaf@alhur.es> Date: Wed, 16 Sep 2020 20:24:17 -0300 Subject: [PATCH 8/9] mypy quibbles. --- lnbits/app.py | 10 +++++----- lnbits/commands.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index 8bda0e8..4a8c1cc 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -34,7 +34,7 @@ def create_app(config_object="lnbits.settings") -> Quart: return app -def register_blueprints(app) -> None: +def register_blueprints(app: Quart) -> None: """Register Flask blueprints / LNbits extensions.""" app.register_blueprint(core_app) @@ -46,12 +46,12 @@ def register_blueprints(app) -> None: raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.") -def register_commands(app): +def register_commands(app: Quart): """Register Click commands.""" app.cli.add_command(db_migrate) -def register_assets(app): +def register_assets(app: Quart): """Serve each vendored asset separately or a bundle.""" @app.before_request @@ -64,13 +64,13 @@ def register_assets(app): g.VENDORED_CSS = ["/static/bundle.css"] -def register_filters(app): +def register_filters(app: Quart): """Jinja filters.""" app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"] app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions() -def register_request_hooks(app): +def register_request_hooks(app: Quart): """Open the core db for each request so everything happens in a big transaction""" @app.before_request diff --git a/lnbits/commands.py b/lnbits/commands.py index 0cc5eff..653175f 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -4,7 +4,7 @@ import re import os import sqlite3 -from scss.compiler import compile_string +from scss.compiler import compile_string # type: ignore from .core import migrations as core_migrations from .db import open_db, open_ext_db From cd8a447ba041657c20323ac27a9aa7c5883c6c95 Mon Sep 17 00:00:00 2001 From: fiatjaf <fiatjaf@alhur.es> Date: Thu, 17 Sep 2020 22:41:30 -0300 Subject: [PATCH 9/9] pytest quibbles and dev dependencies. --- Pipfile | 4 +- Pipfile.lock | 90 +++++++++++++--------------------------- requirements.txt | 2 +- tests/conftest.py | 5 ++- tests/core/test_views.py | 10 +++-- 5 files changed, 42 insertions(+), 69 deletions(-) diff --git a/Pipfile b/Pipfile index 1e81675..0ae2523 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,7 @@ typing-extensions = "*" [dev-packages] black = "==20.8b1" -flake8 = "*" -flake8-mypy = "*" pytest = "*" pytest-cov = "*" +pytest-asyncio = "*" +mypy = "==0.761" diff --git a/Pipfile.lock b/Pipfile.lock index 93ae73b..8101e85 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2c716474f9f263d8e1310ca44c2f50996f3516273b483a40e2b2ad68b8071dd6" + "sha256": "775e2ea809508c83df4696bba3a38a03e288cba22f0a7f562120230f40351ab9" }, "pipfile-spec": 6, "requires": { @@ -229,11 +229,11 @@ }, "marshmallow": { "hashes": [ - "sha256:67bf4cae9d3275b3fc74bd7ff88a7c98ee8c57c94b251a67b031dc293ecc4b76", - "sha256:a2a5eefb4b75a3b43f05be1cca0b6686adf56af7465c3ca629e5ad8d1e1fe13d" + "sha256:2272273505f1644580fbc66c6b220cc78f893eb31f1ecde2af98ad28011e9811", + "sha256:47911dd7c641a27160f0df5fd0fe94667160ffe97f70a42c3cc18388d86098cc" ], "markers": "python_version >= '3.5'", - "version": "==3.7.1" + "version": "==3.8.0" }, "priority": { "hashes": [ @@ -394,8 +394,7 @@ }, "black": { "hashes": [ - "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea", - "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b" + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" ], "index": "pypi", "version": "==20.8b1" @@ -448,22 +447,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.3" }, - "flake8": { - "hashes": [ - "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", - "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" - ], - "index": "pypi", - "version": "==3.8.3" - }, - "flake8-mypy": { - "hashes": [ - "sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18", - "sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc" - ], - "index": "pypi", - "version": "==17.8.0" - }, "iniconfig": { "hashes": [ "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", @@ -471,13 +454,6 @@ ], "version": "==1.0.1" }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, "more-itertools": { "hashes": [ "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", @@ -488,23 +464,23 @@ }, "mypy": { "hashes": [ - "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", - "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", - "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", - "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", - "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", - "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", - "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", - "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", - "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", - "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", - "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", - "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", - "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", - "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" + "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", + "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", + "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", + "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", + "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", + "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", + "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", + "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", + "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", + "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", + "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", + "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", + "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", + "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" ], - "markers": "python_version >= '3.5'", - "version": "==0.782" + "index": "pypi", + "version": "==0.761" }, "mypy-extensions": { "hashes": [ @@ -544,22 +520,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.9.0" }, - "pycodestyle": { - "hashes": [ - "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", - "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.6.0" - }, - "pyflakes": { - "hashes": [ - "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", - "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.2.0" - }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", @@ -576,6 +536,14 @@ "index": "pypi", "version": "==6.0.2" }, + "pytest-asyncio": { + "hashes": [ + "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", + "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" + ], + "index": "pypi", + "version": "==0.14.0" + }, "pytest-cov": { "hashes": [ "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191", diff --git a/requirements.txt b/requirements.txt index d69fad1..55e3fee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ itsdangerous==1.1.0 jinja2==2.11.2 lnurl==0.3.5 markupsafe==1.1.1 -marshmallow==3.7.1 +marshmallow==3.8.0 priority==1.3.0 pydantic==1.6.1 pyscss==1.3.7 diff --git a/tests/conftest.py b/tests/conftest.py index b7770a0..e56006a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,9 +4,10 @@ from lnbits.app import create_app @pytest.fixture -def client(): +@pytest.mark.asyncio +async def client(): app = create_app() app.config["TESTING"] = True - with app.test_client() as client: + async with app.test_client() as client: yield client diff --git a/tests/core/test_views.py b/tests/core/test_views.py index 377d2fd..422b251 100644 --- a/tests/core/test_views.py +++ b/tests/core/test_views.py @@ -1,3 +1,7 @@ -def test_homepage(client): - r = client.get("/") - assert b"Add a new wallet" in r.data +import pytest + + +@pytest.mark.asyncio +async def test_homepage(client): + r = await client.get("/") + assert b"Add a new wallet" in await r.get_data()