Compare commits
105
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cf488ea4f | ||
|
|
e01de0eae5 | ||
|
|
5650731bce | ||
|
|
53d65c0258 | ||
|
|
c4db2d5c28 | ||
|
|
b97595b06d | ||
|
|
dd2a282158 | ||
|
|
3c7e4a3eaf | ||
|
|
89c31139f8 | ||
|
|
2d580a0d0e | ||
|
|
0497c603c0 | ||
|
|
5831fe2ab6 | ||
|
|
dfdb890751 | ||
|
|
3735fe7aa1 | ||
|
|
97a13526d9 | ||
|
|
be04623b9d | ||
|
|
c56784340e | ||
|
|
0603e525ff | ||
|
|
410e96771e | ||
|
|
784a2dd876 | ||
|
|
bc930ab606 | ||
|
|
f518e350a6 | ||
|
|
8055e6a73c | ||
|
|
6b2061f107 | ||
|
|
ce2f764432 | ||
|
|
efe4b91877 | ||
|
|
55a9dc749a | ||
|
|
bc984ab0db | ||
|
|
1e36a8a393 | ||
|
|
c17ac32c86 | ||
|
|
c73fbe1b27 | ||
|
|
2ddc23de04 | ||
|
|
294beb2fca | ||
|
|
6c286cd265 | ||
|
|
68e41789ce | ||
|
|
e746dcd787 | ||
|
|
61f736878c | ||
|
|
f51d44e99b | ||
|
|
95b0ba536b | ||
|
|
5e81c64831 | ||
|
|
d56c2627f4 | ||
|
|
c3281399d2 | ||
|
|
8ea72c9934 | ||
|
|
aed2414f3a | ||
|
|
76f17d8594 | ||
|
|
85e617aae9 | ||
|
|
68d94a43d0 | ||
|
|
29dc33dd71 | ||
|
|
a54bff6640 | ||
|
|
d2874a90e9 | ||
|
|
c099885bbb | ||
|
|
b296c38795 | ||
|
|
9af4161679 | ||
|
|
76510b7892 | ||
|
|
ea3b858695 | ||
|
|
41277e6931 | ||
|
|
8add56a24c | ||
|
|
e768e4b075 | ||
|
|
a40823de43 | ||
|
|
171e639109 | ||
|
|
6f4cac0c41 | ||
|
|
97dff38af4 | ||
|
|
13f01dfbe6 | ||
|
|
14d61eaf56 | ||
|
|
63b9741c14 | ||
|
|
22451ca806 | ||
|
|
e73a508011 | ||
|
|
2fac47c05a | ||
|
|
b8dd151043 | ||
|
|
42d307adf9 | ||
|
|
c3ce18c8d9 | ||
|
|
d914c3f701 | ||
|
|
52aff1128e | ||
|
|
9eb253fe55 | ||
|
|
35fb524e1e | ||
|
|
01c8e8a23f | ||
|
|
b37af0c600 | ||
|
|
a61d346669 | ||
|
|
8530413537 | ||
|
|
3a6034e8f6 | ||
|
|
d9fe80d41b | ||
|
|
4730500ed7 | ||
|
|
083f7a0a8d | ||
|
|
3a1167c892 | ||
|
|
236bcac263 | ||
|
|
2c5e539795 | ||
|
|
5553ed82f0 | ||
|
|
9fadb68fed | ||
|
|
d496869572 | ||
|
|
9f33ed49e5 | ||
|
|
ea480866ea | ||
|
|
690b33e096 | ||
|
|
d72678972d | ||
|
|
8dfdd15adb | ||
|
|
8442f277c4 | ||
|
|
8f76da66e8 | ||
|
|
f7ca28db34 | ||
|
|
c951577962 | ||
|
|
0847a1c933 | ||
|
|
f731bd766e | ||
|
|
59819e2875 | ||
|
|
e7184346ab | ||
|
|
178fe65fb2 | ||
|
|
1c8beba80b | ||
|
|
1516a14a0d |
+2
-1
@@ -2,13 +2,14 @@ FLASK_APP=lnbits
|
||||
FLASK_ENV=development
|
||||
|
||||
LNBITS_SITE_TITLE=LNbits
|
||||
LNBITS_ALLOWED_USERS="all"
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
LNBITS_DATA_FOLDER="/your_custom_data_folder"
|
||||
LNBITS_DISABLED_EXTENSIONS="amilk,events"
|
||||
LNBITS_FORCE_HTTPS=1
|
||||
LNBITS_SERVICE_FEE="0.0"
|
||||
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet, CLightningWallet, LnbitsWallet
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), LndRestWallet, CLightningWallet, LnbitsWallet
|
||||
LNBITS_BACKEND_WALLET_CLASS=LntxbotWallet
|
||||
|
||||
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
lnbits/static/vendor/** linguist-vendored
|
||||
lnbits/extensions/**/static/vendor/** linguist-vendored
|
||||
**/static/vendor/** linguist-vendored
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
custom: https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK
|
||||
@@ -0,0 +1,54 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 12 * * 5'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Run Linters
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
mypy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Run MyPy python type checker
|
||||
uses: jpetrucciani/mypy-check@master
|
||||
with:
|
||||
path: 'lnbits'
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Check JS code formatting convention
|
||||
uses: creyD/prettier_action@v2.2
|
||||
with:
|
||||
dry: True
|
||||
prettier_options: --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**
|
||||
@@ -1,13 +0,0 @@
|
||||
name: mypy type checker
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: jpetrucciani/mypy-check@master
|
||||
with:
|
||||
path: 'lnbits'
|
||||
@@ -0,0 +1,38 @@
|
||||
name: test suite
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Test with pytest
|
||||
env:
|
||||
LNBITS_BACKEND_WALLET_CLASS: LNPayWallet
|
||||
LNBITS_FORCE_HTTPS: 0
|
||||
LNPAY_API_ENDPOINT: https://lnpay.co/v1/
|
||||
LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
|
||||
LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
|
||||
LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
|
||||
LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
|
||||
run: |
|
||||
pip install pytest pytest-cov
|
||||
pytest --cov=lnbits --cov-report=xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
@@ -27,3 +27,5 @@ venv
|
||||
.pyre*
|
||||
|
||||
__bundle__
|
||||
|
||||
node_modules
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSpacing": false
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
For developers
|
||||
==============
|
||||
|
||||
LNbits uses [Flask](http://flask.pocoo.org/).
|
||||
Feel free to contribute to the project.
|
||||
|
||||
Application dependencies
|
||||
------------------------
|
||||
The application uses [Pipenv][pipenv] to manage Python packages.
|
||||
While in development, you will need to install all dependencies:
|
||||
|
||||
$ pipenv shell
|
||||
$ pipenv install --dev
|
||||
|
||||
Running the server
|
||||
------------------
|
||||
|
||||
$ flask run
|
||||
|
||||
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
|
||||
|
||||
Style guide
|
||||
-----------
|
||||
Tab size is 4 spaces. Maximum line length is 120. You should run `black` before commiting any change.
|
||||
|
||||
$ black lnbits
|
||||
|
||||
[pipenv]: https://docs.pipenv.org/#install-pipenv-today
|
||||
@@ -0,0 +1,2 @@
|
||||
prettier:
|
||||
./node_modules/.bin/prettier --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**
|
||||
@@ -9,15 +9,14 @@ python_version = "3.7"
|
||||
[packages]
|
||||
bitstring = "*"
|
||||
cerberus = "*"
|
||||
lnd-grpc = "*"
|
||||
lnurl = "*"
|
||||
flask = "*"
|
||||
flask-assets = "*"
|
||||
flask-compress = "*"
|
||||
flask-cors = "*"
|
||||
flask-talisman = "*"
|
||||
gevent = "*"
|
||||
gevent = "==20.5.0"
|
||||
gunicorn = "*"
|
||||
pylightning = "*"
|
||||
pyscss = "*"
|
||||
requests = "*"
|
||||
shortuuid = "*"
|
||||
@@ -28,4 +27,3 @@ flake8 = "*"
|
||||
flake8-mypy = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-sugar = "*"
|
||||
|
||||
Generated
+271
-347
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "1c40bedbf1f2a9e9bccd443b57bd6dda496361df8712ddda5efcec0d00cab9f1"
|
||||
"sha256": "76587c919e4a0d49f3a4ff297353ec8b8c92521effdb5eaf985cb32213ebc5c5"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -21,16 +21,53 @@
|
||||
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
|
||||
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"bitstring": {
|
||||
"hashes": [
|
||||
"sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443",
|
||||
"sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf",
|
||||
"sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096"
|
||||
"sha256:fdf3eb72b229d2864fb507f8f42b1b2c57af7ce5fec035972f9566de440a864a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.6"
|
||||
"version": "==3.1.7"
|
||||
},
|
||||
"brotli": {
|
||||
"hashes": [
|
||||
"sha256:0538dc1744fd17c314d2adc409ea7d1b779783b89fd95bcfb0c2acc93a6ea5a7",
|
||||
"sha256:0970a47f471782912d7705160b2b0a9306e68e6fadf9cffcaeb42d8f0951e26c",
|
||||
"sha256:113f51658e6fe548dce4b3749f6ef6c24de4184ba9c10a909cbee4261c2a5da0",
|
||||
"sha256:1e1aa9c4d1558889f42749c8baf846007953bfd32c8209230cf1cd1f5ef33495",
|
||||
"sha256:2f2f4f78f29ac4a45d15b3d9fc3fd9705e0ad313a44b129f6e1d0c6916bad0e2",
|
||||
"sha256:3269f6de1dd150fd0cce1c158b61ff5ac06d627fd3ae9c6ea03aed26fbbff7ea",
|
||||
"sha256:3f4a1f6240916c7984c7f2542786710f622992508dafee0b1714e6d340fb9ffd",
|
||||
"sha256:50dd9ad2a2bb12da4e9002a438672d182f98e546e99952de80280a1e1729664f",
|
||||
"sha256:5519a4b01b1a4f965083cbfa2ef2b9774c5a5f352341c47b50776ad109423d72",
|
||||
"sha256:5eb27722d320370315971c427eb8aa7cc0791f2a458840d357ac653bd0ad3a14",
|
||||
"sha256:5f06b4d5b6f58e5b5c220c2f23cad034dc5efa51b01fde2351ced1605bd980e2",
|
||||
"sha256:71ceee286ea7ec613f1c36f1c6181864a6ca24ebb55e371276f33d6af8742834",
|
||||
"sha256:72848d25a5f9e736db4af4512e0c3feecc094d57d241f8f1ae959115a2c39756",
|
||||
"sha256:743001bca75f4a6b4454be3510feca46f9d61a0c782a9bc2bc684bdb245e279e",
|
||||
"sha256:7ac98c71a15648fd11bc1f32608b6110e396121280790082e32b9a3109048bc6",
|
||||
"sha256:9d1c2dd27a1083fefd05b1b2f8df4a6bc2aaa6c21dd82cd41c8ae5e7c23a87f8",
|
||||
"sha256:a13ce9b419fe9f277c63f700efb0e444331509d1881b5610d2ba7e9080606967",
|
||||
"sha256:a19ef0952b9d2803df88dff07f45a6c92d5676afb9b8d69cf32232d684036d11",
|
||||
"sha256:ad766ca8b8c1419b71a22756b45264f45725c86133dc80a7cbe30b6b78c75620",
|
||||
"sha256:ad7963f261988ee0883816b6b9f206f11461c9b3cb5cfbca0c9ab5adc406d395",
|
||||
"sha256:af0451e23016631a2f52925a10d738ac4a0f794ac315c30380b22efc0c90cbc6",
|
||||
"sha256:c16201060c5a3f8742e3deae759014251ac92f382f82bc2a41dc079ff18c3f24",
|
||||
"sha256:c43b202f65891861a9a336984a103de25de235f756de69e32db893156f767013",
|
||||
"sha256:c675c6cce4295cb1a692f3de7416aacace7314e064b94bc86e93aceefce7fd3e",
|
||||
"sha256:d17cec0b992b1434f5f9df9986563605a4d1b1acd5574c87fc2ac014bcbd3316",
|
||||
"sha256:dc91f6129953861a73d9a65c52a8dd682b561a9ebaf65283541645cab6489917",
|
||||
"sha256:e2f4cbd1760d2bf2f30e396c2301999aab0191aec031a6a8a04950b2f575a536",
|
||||
"sha256:f192e6d3556714105c10486bbd6d045e38a0c04d9da3cef21e0a8dfd8e162df4",
|
||||
"sha256:f775b07026af2b1b0b5a8b05e41571cdcf3a315a67df265d60af301656a5425b",
|
||||
"sha256:f969ec7f56ba9636679e69ca07fba548312ccaca37412ee823c7f413541ad7e0",
|
||||
"sha256:f9dc52cd70907aafb99a773b66b156f2f995c7a0d284397c487c8b71ddbef2f9",
|
||||
"sha256:f9ee88bb52352588ceb811d045b5c9bb1dc38927bc150fd156244f60ff3f59f1",
|
||||
"sha256:fc7212e36ebeb81aebf7949c92897b622490d7c0e333a479c0395591e7994600"
|
||||
],
|
||||
"version": "==1.0.7"
|
||||
},
|
||||
"cerberus": {
|
||||
"hashes": [
|
||||
@@ -41,10 +78,10 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
|
||||
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.4.5.1"
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -55,10 +92,11 @@
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
|
||||
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"version": "==7.1.1"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
@@ -78,10 +116,18 @@
|
||||
},
|
||||
"flask-compress": {
|
||||
"hashes": [
|
||||
"sha256:468693f4ddd11ac6a41bca4eb5f94b071b763256d54136f77957cfee635badb3"
|
||||
"sha256:f367b2b46003dd62be34f7fb1379938032656dca56377a9bc90e7188e4289a7c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.0"
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"flask-cors": {
|
||||
"hashes": [
|
||||
"sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
|
||||
"sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.8"
|
||||
},
|
||||
"flask-talisman": {
|
||||
"hashes": [
|
||||
@@ -93,139 +139,54 @@
|
||||
},
|
||||
"gevent": {
|
||||
"hashes": [
|
||||
"sha256:0b84a8d6f088b29a74402728681c9f11864b95e49f5587a666e6fbf5c683e597",
|
||||
"sha256:1ef086264e846371beb5742ebaeb148dc96adf72da2ff350ae5603421cdc2ad9",
|
||||
"sha256:2070c65896f89a85b39f49427d6132f7abd047129fc4da88b3670f0ba13b0cf7",
|
||||
"sha256:2fbe0bc43d8c5540153f06eece6235dda14e5f99bdd9183838396313100815d7",
|
||||
"sha256:32813de352918fb652a3db805fd6e08e0a1666a1a9304eef95938c9c426f9573",
|
||||
"sha256:38c45d8a3b647f56f8a68769a8ac4953be84a84735c7c7a4d7ca62022bd54036",
|
||||
"sha256:3b4c4d99f87c0d04b825879c5a91fbfa2b66da7c25b8689e9bdd9f4741d5f80d",
|
||||
"sha256:42cae3be36b7458f411bd589c66aaba27e4e611ec3d3621e37fd732fe383f9b6",
|
||||
"sha256:4572dc7907a0ac3c39b9f0898dbdf390ae3250baaae5f7395661fb844e2e23be",
|
||||
"sha256:6088bedd8b6bcdb815be322304a5d1c028ffa837d84e93b349928dadac62f354",
|
||||
"sha256:8a9aba59a3268f20c7b584119215bdc589cb81500d93dad4dab428eb02f72944",
|
||||
"sha256:8cca7ffd58559f8d51e5605ad73afcc6f348f9747d2fa539b336e70851b69b79",
|
||||
"sha256:956e82a5d0e90f8d71efe4cecccde602cfb657cd866c58bb953c9c30ca1b3d77",
|
||||
"sha256:b0aea12de542f8fcd6882087bdd5b4d7dc8bb316d28181f6b012dd0b91583285",
|
||||
"sha256:b46399f6c9eccc2e6de1dc1057d362be840443e5439b06cce8b01d114ba1a7ec",
|
||||
"sha256:c0b38a654c8fde5b9d9bd27ea3261aeefe36bc9244b170b6d3b11d72a2163bdb",
|
||||
"sha256:c516cc5d70c3faf07f271d50930d144339c69fb80f3cac9b687aa964e518535e",
|
||||
"sha256:c7a62d51c6dca84f91a91b940037523c926a516f0568f47dc1386bd1682cf4e9",
|
||||
"sha256:cea28f958bc4206ae092043e0775cd7a2bb2536bcbece292732c6484c1076c01",
|
||||
"sha256:d56f36eb98532d2bccc51cb0964c31e9fbd9b2282074c297dc9b006b047e2966",
|
||||
"sha256:de6c0cbcb890d0a79323961d3b593a0f2f54dcb9fe38ee5167f2d514e69e3c8c",
|
||||
"sha256:e0990009e7c1624f9a0f3335df1ab8d45678241c852659ac645b70ed8229097c",
|
||||
"sha256:e7d23d5f32c9db6ae49c4b58585618dcafd6ad0babae251c9c8297afebc4744b",
|
||||
"sha256:ee39caf14d66e619709cdfe3962bc68a234518e43ea8c811c0d67a864bc7c196"
|
||||
"sha256:00b03601b8dd1ee2aa07811cb60a4befe36173b15d91c6e207e37f8d77dd6fac",
|
||||
"sha256:0acc15ba2ac2a555529ad82d5a28fc85dbb6b2ff947657d67bebfd352e2b5c14",
|
||||
"sha256:15eae3cd450dac7dae7f4ac59e01db1378965c9ef565c39c5ae78c5a888f9ac9",
|
||||
"sha256:1dc7f1f6bc1f67d625e4272b01e717eba0b4fa024d2ff7934c8d320674d6f7fa",
|
||||
"sha256:1dd95433be45e1115053878366e3f5332ae99c39cb345be23851327c062b9f4a",
|
||||
"sha256:28b7d83b4327ceb79668eca2049bf4b9ce66d5ace18a88335e3035b573f889fd",
|
||||
"sha256:31dc5d4ab8172cc00c4ff17cb18edee633babd961f64bf54214244d769bc3a74",
|
||||
"sha256:38db524ea88d81d596b2cbb6948fced26654a15fec40ea4529224e239a6f45e8",
|
||||
"sha256:3ff477b6d275396123faf8ce2d5b82f96d85ba264e0b9d4b56a2bac49d1b9adc",
|
||||
"sha256:4d2729dd4bf9c4d0f29482f53cdf9fc90a498aebb5cd7ae8b45d35657437d2ac",
|
||||
"sha256:52e5cd607749ed3b8aa0272cacf2c11deec61fca4c3bec57a9fea8c49316627d",
|
||||
"sha256:5c604179cebcc57f10505d8db177b92a715907815a464b066e7eba322d1c33ac",
|
||||
"sha256:88c76df4967c5229f853aa67ad1b394d9e4f985b0359c9bc9879416bba3e7c68",
|
||||
"sha256:929c33df8e9bcbe31906024fcd21580bd018196dbd3249eb5b2f19d63e11092d",
|
||||
"sha256:92edc18a357473e01a4e4a82c073ed3c99ceca6e3ce93c23668dd4a2401f07dc",
|
||||
"sha256:937d36730f2b0dee3387712074b1f15b802e2e074a3d7c6dcaf70521236d607c",
|
||||
"sha256:9b4e940fc6071afebb86ba5f48dbb5f1fc3cb96ebeb8cf145eb5b499e9c6ee33",
|
||||
"sha256:a7805934e8ce81610b61f806572c3d504cedd698cc8c9460d78d2893ba598c4a",
|
||||
"sha256:d07a2afe4215731eb57d5b257a2e7e7e170d8a7ae1f02f6d0682cd3403debea9",
|
||||
"sha256:e01d5373528e4ebdde66dc47a608d225fa3c4408ccd828d26c49b7ff75d82bd9",
|
||||
"sha256:efd9546468502a30ddd4699c3124ccb9d3099130f9b5ae1e2a54ad5b46e86120",
|
||||
"sha256:fcb64f3a28420d1b872b7ef41b12e8a1a4dcadfc8eff3c09993ab0cdf52584a1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.4.0"
|
||||
},
|
||||
"googleapis-common-protos": {
|
||||
"hashes": [
|
||||
"sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e"
|
||||
],
|
||||
"version": "==1.51.0"
|
||||
"version": "==20.5.0"
|
||||
},
|
||||
"greenlet": {
|
||||
"hashes": [
|
||||
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
|
||||
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
|
||||
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
|
||||
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
|
||||
"sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e",
|
||||
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
|
||||
"sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122",
|
||||
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
|
||||
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
|
||||
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
|
||||
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
|
||||
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
|
||||
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
|
||||
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
|
||||
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
|
||||
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
|
||||
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
|
||||
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
|
||||
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
|
||||
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
|
||||
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656",
|
||||
"sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef"
|
||||
"sha256:1000038ba0ea9032948e2156a9c15f5686f36945e8f9906e6b8db49f358e7b52",
|
||||
"sha256:133ba06bad4e5f2f8bf6a0ac434e0fd686df749a86b3478903b92ec3a9c0c90b",
|
||||
"sha256:1429dc183b36ec972055e13250d96e174491559433eb3061691b446899b87384",
|
||||
"sha256:1b805231bfb7b2900a16638c3c8b45c694334c811f84463e52451e00c9412691",
|
||||
"sha256:3a35e33902b2e6079949feed7a2dafa5ac6f019da97bd255842bb22de3c11bf5",
|
||||
"sha256:5ea034d040e6ab1d2ae04ab05a3f37dbd719c4dee3804b13903d4cc794b1336e",
|
||||
"sha256:682328aa576ec393c1872615bcb877cf32d800d4a2f150e1a5dc7e56644010b1",
|
||||
"sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c",
|
||||
"sha256:7eed31f4efc8356e200568ba05ad645525f1fbd8674f1e5be61a493e715e3873",
|
||||
"sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872",
|
||||
"sha256:b0b2a984bbfc543d144d88caad6cc7ff4a71be77102014bd617bd88cfb038727",
|
||||
"sha256:c196a5394c56352e21cb7224739c6dd0075b69dd56f758505951d1d8d68cf8a9",
|
||||
"sha256:d83c1d38658b0f81c282b41238092ed89d8f93c6e342224ab73fb39e16848721",
|
||||
"sha256:df7de669cbf21de4b04a3ffc9920bc8426cab4c61365fa84d79bf97401a8bef7",
|
||||
"sha256:e5db19d4a7d41bbeb3dd89b49fc1bc7e6e515b51bbf32589c618655a0ebe0bf0",
|
||||
"sha256:e695ac8c3efe124d998230b219eb51afb6ef10524a50b3c45109c4b77a8a3a92",
|
||||
"sha256:eac2a3f659d5f41d6bbfb6a97733bc7800ea5e906dc873732e00cebb98cec9e4"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython'",
|
||||
"version": "==0.4.15"
|
||||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:085bbf7fd0070b8d65e84aa32979f17cfe624d27b5ce23955ef770c19d2d9623",
|
||||
"sha256:0ae207a47ec0ad66eb1f53a27d566674d13a236c62ced409891335318ea9b8c5",
|
||||
"sha256:0c130204ff5de0b9f041bf3126db0d29369d69883592e4b0d3c19868ba0ced7e",
|
||||
"sha256:0ef6b380a588c2c6b29c6cfa0ba7f5d367beb33d5504bcc68658fa241ad498d2",
|
||||
"sha256:16e1edb367763ea08d0994d4635ec05f4f8db9db59c39304b061097e3b93df43",
|
||||
"sha256:16f5523dacae5aaeda4cf900da7e980747f663298c38c18eb4e5317704aa007a",
|
||||
"sha256:181b5078cf568f37915b8a118afcef5fc9f3128c59c38998ed93e7dd793e3928",
|
||||
"sha256:245564713cb4ac7bccb0f11be63781beb62299a44d8ab69031c859dbd9461728",
|
||||
"sha256:271abbe28eb99fa5c70b3f272c0c66b67dab7bb11e1d29d8e616b4e0e099d29a",
|
||||
"sha256:2e1b01cba26988c811c7fb91a0bca19c9afb776cc3d228993f08d324bdd0510a",
|
||||
"sha256:3366bd6412c1e73acb1ee27d7f0c7d7dbee118ad8d98c957c8173691b2effeec",
|
||||
"sha256:3893b39a0a17d857dc3a42fdb02a26aa53a59bfce49987187bcc0261647f1f55",
|
||||
"sha256:3c7864d5ae63b787001b01b376f6315aef1a015aa9c809535235ed0ead907919",
|
||||
"sha256:42c6716adf3ec1f608b2b56e885f26dd86e80d2fc1617f51fc92d1b0b649e28e",
|
||||
"sha256:4bef0756b9e0df78e8d67a5b1e0e89b7daf41525d575f74e1f14a993c55b680d",
|
||||
"sha256:4fe081862e58b8fbef0e479aefc9a64f8f17f53074df1085d8c1fe825a6e5df4",
|
||||
"sha256:505a8d1b4ac571a51f10c4c995d5d4714f03c886604dc3c097ef5fd57bcfcf0b",
|
||||
"sha256:5c2e81b6ab9768c43f2ca1c9a4c925823aad79ae95efb351007df4b92ebce592",
|
||||
"sha256:70ff2df0c1795c5cf585a72d95bb458838b40bad5653c314b9067ba819e918f9",
|
||||
"sha256:97b5612fc5d4bbf0490a2d80bed5eab5b59112ef1640440c1a9ac824bafa6968",
|
||||
"sha256:a35f8f4a0334ed8b05db90383aecef8e49923ab430689a4360a74052f3a89cf4",
|
||||
"sha256:aafe85a8210dfa1da3c46831b7f00c3735240b7b028eeba339eaea6ffdb593fb",
|
||||
"sha256:c2e53eb253840f05278a8410628419ba7060815f86d48c9d83b6047de21c9956",
|
||||
"sha256:c3645887db3309fc87c3db740b977d403fb265ebab292f1f6a926c4661231fd5",
|
||||
"sha256:c6565cc92853af13237b2233f331efdad07339d27fe1f5f74256bfde7dc2f587",
|
||||
"sha256:cbc322c5d5615e67c2a15be631f64e6c2bab8c12505bc7c150948abdaa0bdbac",
|
||||
"sha256:df749ee982ec35ab76d37a1e637b10a92b4573e2b4e1f86a5fa8a1273c40a850",
|
||||
"sha256:e9439d7b801c86df13c6cbb4c5a7e181c058f3c119d5e119a94a5f3090a8f060",
|
||||
"sha256:f493ac4754717f25ace3614a51dd408a32b8bff3c9c0c85e9356e7e0a120a8c8",
|
||||
"sha256:f80d10bdf1a306f7063046321fd4efc7732a606acdd4e6259b8a37349079b704",
|
||||
"sha256:f83b0c91796eb42865451a20e82246011078ba067ea0744f7301e12a94ae2e1b"
|
||||
],
|
||||
"version": "==1.28.1"
|
||||
},
|
||||
"grpcio-tools": {
|
||||
"hashes": [
|
||||
"sha256:095911bd21dd2dc029b8862b44e65b41341135fd16da60ad9d0df4b8e9a0618e",
|
||||
"sha256:17a6b63ee1650abbbc9306f3144d1d3bd3d088a270ca6569afcb68a8cc4a4a38",
|
||||
"sha256:27fcab84ea948d965f82c7994903e8e36a20ce4ad7ef55a22c8d7c639e6c8808",
|
||||
"sha256:2ffa5f6fbd26ea6c7626bf89d6ef4d64d111000a8bd4e9df69b070093de0f85d",
|
||||
"sha256:491dcd98d988c0a4ed9bbb1ec6310588fc0e16b334b64119b973363a1cf96cc1",
|
||||
"sha256:493b5a275ff2692942951926a259e396ff13acec72e6d93498638006b1a1bd71",
|
||||
"sha256:4c980208c1620e52e86c4037058097673ee8e3bbbec34582f2538e614d0c41f4",
|
||||
"sha256:55f3a86c3a808b8a5c2dc8112c45329dd912e43a6fb474da94a7e04d8bb36e1a",
|
||||
"sha256:69a60e1c020547668f313b7a81d653f8c820066ab0f6ed979af760905b61d9e1",
|
||||
"sha256:6f7806fe53e5979fa10ec49cbd5236cf0e57c8817ce54350bccc8ebceaeb54fb",
|
||||
"sha256:8f752fa03eb2d851aac5838aa46a5b831660e9800634fad8ccd46539e9cb4663",
|
||||
"sha256:9210b3575fee355a4056cff8ff7212b823371e9fa9ad1467e3c0c5cb006d0d7d",
|
||||
"sha256:9b900cf90b293acd48bf258d5e416eae9540b5aa0c75cc2fe68a7e2c5f4834ae",
|
||||
"sha256:addc40a47f496cafa108ba1a31838a9832223e0dcafaf424d38e6b8c79fe26ac",
|
||||
"sha256:adf089aaf6e21358b12e39d9fa7c28611340d8399a918c0b72ff122ce9b7e0af",
|
||||
"sha256:b22172e3ad5e849815f3e35a002fb67c52e83cd3e4299cf45760de4674c7cd04",
|
||||
"sha256:b5cf457dcd6499f8c2f2bdcc486d681ca3ee673064484a31c507df6139bf00fe",
|
||||
"sha256:b5ee6f089bb040d175e658911760584c7a6cb70fbdcc78bdbe6349475933483b",
|
||||
"sha256:bb9b1a05917011e98e5331d129a40a41f49c5652e1b86d5fea9b3f6c98454928",
|
||||
"sha256:c527db050fa619a2bd293a73db0e0af42f0bd5a4f41c043fc5da543d81f5fed6",
|
||||
"sha256:c9efe6420d2ffaf9aa32d292817ddfc939b9adf28a51cef30984bab02ed35a58",
|
||||
"sha256:cb7b54a96f4d9aae5d9c6b644a69ce48c12cfa3558c86ce0a62d9c8a08a83f2e",
|
||||
"sha256:d661e5c4f82f9a223b9d1a6c9ec68faea060324e0d25684a6f7fb0b9622ab561",
|
||||
"sha256:d6ee6f338e0099976161e78ecf91d2ab9c04be75a944ef29facc93a4aad28e1d",
|
||||
"sha256:d89dec894495c51ee0a8dd0c896e4320e95bab179c3faa6a439ef5719486c35e",
|
||||
"sha256:dc7ea3739aa6f5abe6e9a27f403b30c7996978176e5884c41ff3d8a153e7c35a",
|
||||
"sha256:de10f8b28ef3e8a7f336e02b7c2912713bd41e0931c4fad3a6993d25c8511159",
|
||||
"sha256:e05f7bfb63b844b7f9d8b50019b02168fd8982278dc0443986f3504b3cb87d2a",
|
||||
"sha256:e942dd349d5be0d5ebbe96ed9cbcfb5fdb0bc7b57357754c42db8083a0ec80aa",
|
||||
"sha256:edc01f022a878722ae69bff3faa8c6ec7e967d57b7e8a5f4b2f6100ad903a978",
|
||||
"sha256:f61d219d06057e015d662c0b40438f119f58e514a5bebf425058a34d05ff21db"
|
||||
],
|
||||
"version": "==1.28.1"
|
||||
"version": "==0.4.16"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@@ -237,16 +198,18 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
@@ -254,23 +217,15 @@
|
||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"lnd-grpc": {
|
||||
"hashes": [
|
||||
"sha256:43114aead2dc2d008751231784fc94027d60569a5e4dc86ed68032a2203fe781",
|
||||
"sha256:c28208d912b28f5b0b11fa87559c6e564731549ecbc52eafe7d5dd2b2f469cf8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"lnurl": {
|
||||
"hashes": [
|
||||
"sha256:633ddedddcff4bd8b7790eba1649ec10c91fb28e690e3f6e984cb8591be3b626",
|
||||
"sha256:749e0d73a8e5d41b03c734df4f22c0e022f1da4f64e600a898b56e1bb75f5dda"
|
||||
"sha256:398fcb58b8112575bb24a445c61dda0e180df232717ccdbcb6a6932221cc6d16"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.3"
|
||||
"version": "==0.3.4"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
@@ -308,60 +263,31 @@
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
|
||||
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
|
||||
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
|
||||
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
|
||||
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
|
||||
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
|
||||
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
|
||||
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
|
||||
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
|
||||
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
|
||||
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
|
||||
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
|
||||
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
|
||||
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
|
||||
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
|
||||
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
|
||||
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
|
||||
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
|
||||
],
|
||||
"version": "==3.11.3"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:0b7aadfa1de28057656064e04d9f018d1b186fe2a8e953a2fb41545873b7cf95",
|
||||
"sha256:0f61e67291b99a927816558a218a4e794db72a33621c836e63d12613a2202cd4",
|
||||
"sha256:20946280c750753b3e3177c748825ef189d7ab86c514f6a0b118621110d5f0d3",
|
||||
"sha256:22139ee446992c222977ac0a9269c4da2e9ecc1834f84804ebde008a4649b929",
|
||||
"sha256:3c0f39e884d7a3572d5cc8322b0fe9bf66114283e22e05a5c4b8961c19588945",
|
||||
"sha256:446ce773a552a2cb90065d4aa645e16fa7494369b5f0d199e4d41a992a98204d",
|
||||
"sha256:475e6606873e40717cc3b0eebc7d1101cbfc774e01dadeeea24c121eb5826b86",
|
||||
"sha256:66124752662de0479a9d0c17bdebdc8a889bccad8846626fb66d8669e8eafb63",
|
||||
"sha256:896637b7d8e4cdc0bcee1704fcadacdd167c35ac29f02a4395fce7a033925f26",
|
||||
"sha256:9af44d06db33896a2176603c9cb876df3a60297a292a24d3018956a910cc1402",
|
||||
"sha256:9e46fac8a4674db0777fd0133aa56817e1481beee50971bab39dded7639f9b2b",
|
||||
"sha256:ae206e103e976c40ec294cd6c8fcbfbdaced3ab9b736bc53d03fa11b5aaa1628",
|
||||
"sha256:b11d0bd7ecf41098894e8777ee623c29554dbaa37e862c51bcc5a2b950d1bf77",
|
||||
"sha256:d73070028f7b046a5b2e611a9799c238d7bd245f8fe30f4ad7ff29ddb63aac40",
|
||||
"sha256:ddedcdf9d5c24939578449a8e099ceeec3b3d76243fc143aff63ebf6d5aade10",
|
||||
"sha256:e08e21f4d5395ac17cde19de26be63fb16fb870f0cfde1481ddc22d5e2353548",
|
||||
"sha256:e6239199b363bc53262bcb57f1441206d4b2d46b392eccba2213d8358d6e284a"
|
||||
"sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c",
|
||||
"sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e",
|
||||
"sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e",
|
||||
"sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1",
|
||||
"sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d",
|
||||
"sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9",
|
||||
"sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614",
|
||||
"sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99",
|
||||
"sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633",
|
||||
"sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73",
|
||||
"sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7",
|
||||
"sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b",
|
||||
"sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d",
|
||||
"sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df",
|
||||
"sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20",
|
||||
"sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1",
|
||||
"sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"
|
||||
],
|
||||
"version": "==1.5"
|
||||
},
|
||||
"pylightning": {
|
||||
"hashes": [
|
||||
"sha256:5b7df2c6f0faf17450ea3024f29f901a114d4c68da116a1b130958df510a0557",
|
||||
"sha256:6a07b62b27e01baaf51165e2dfb8b7d1d47f61aef8304a9da3ca5081608a32c0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0.7.3"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.6.1"
|
||||
},
|
||||
"pyscss": {
|
||||
"hashes": [
|
||||
@@ -372,11 +298,11 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
|
||||
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.23.0"
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"shortuuid": {
|
||||
"hashes": [
|
||||
@@ -388,10 +314,11 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
@@ -404,10 +331,11 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
|
||||
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
],
|
||||
"version": "==1.25.9"
|
||||
"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": [
|
||||
@@ -421,22 +349,24 @@
|
||||
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"black": {
|
||||
@@ -449,61 +379,59 @@
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
|
||||
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"version": "==7.1.1"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
|
||||
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
|
||||
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
|
||||
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
|
||||
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
|
||||
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
|
||||
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
|
||||
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
|
||||
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
|
||||
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
|
||||
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
|
||||
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
|
||||
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
|
||||
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
|
||||
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
|
||||
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
|
||||
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
|
||||
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
|
||||
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
|
||||
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
|
||||
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
|
||||
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
|
||||
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
|
||||
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
|
||||
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
|
||||
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
|
||||
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
|
||||
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
|
||||
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
|
||||
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
|
||||
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
|
||||
"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"
|
||||
],
|
||||
"version": "==5.1"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.3"
|
||||
"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"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
||||
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
|
||||
"sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
|
||||
"sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.9"
|
||||
"version": "==3.8.3"
|
||||
},
|
||||
"flake8-mypy": {
|
||||
"hashes": [
|
||||
@@ -515,11 +443,18 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
|
||||
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
|
||||
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
|
||||
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.6.0"
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||
"sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"
|
||||
],
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
@@ -530,29 +465,31 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
|
||||
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
|
||||
"sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5",
|
||||
"sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"
|
||||
],
|
||||
"version": "==8.2.0"
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==8.4.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2",
|
||||
"sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1",
|
||||
"sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164",
|
||||
"sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761",
|
||||
"sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce",
|
||||
"sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27",
|
||||
"sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754",
|
||||
"sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae",
|
||||
"sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9",
|
||||
"sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600",
|
||||
"sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65",
|
||||
"sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8",
|
||||
"sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913",
|
||||
"sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"
|
||||
"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"
|
||||
],
|
||||
"version": "==0.770"
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.782"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -563,10 +500,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
|
||||
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
|
||||
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||
],
|
||||
"version": "==20.3"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.4"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@@ -580,105 +518,97 @@
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
||||
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
],
|
||||
"version": "==1.8.1"
|
||||
"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:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
"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:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
||||
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
|
||||
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
||||
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
||||
],
|
||||
"version": "==2.1.1"
|
||||
"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",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
|
||||
"sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
|
||||
"sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4",
|
||||
"sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.4.1"
|
||||
"version": "==6.0.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
|
||||
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
|
||||
"sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191",
|
||||
"sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"pytest-sugar": {
|
||||
"hashes": [
|
||||
"sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283",
|
||||
"sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.2"
|
||||
"version": "==2.10.1"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b",
|
||||
"sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8",
|
||||
"sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3",
|
||||
"sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e",
|
||||
"sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683",
|
||||
"sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1",
|
||||
"sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142",
|
||||
"sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3",
|
||||
"sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468",
|
||||
"sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e",
|
||||
"sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3",
|
||||
"sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a",
|
||||
"sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f",
|
||||
"sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6",
|
||||
"sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156",
|
||||
"sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b",
|
||||
"sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db",
|
||||
"sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd",
|
||||
"sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a",
|
||||
"sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948",
|
||||
"sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"
|
||||
"sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
|
||||
"sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
|
||||
"sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
|
||||
"sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
|
||||
"sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
|
||||
"sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
|
||||
"sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
|
||||
"sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
|
||||
"sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
|
||||
"sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
|
||||
"sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
|
||||
"sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
|
||||
"sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
|
||||
"sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
|
||||
"sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
|
||||
"sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
|
||||
"sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
|
||||
"sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
|
||||
"sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
|
||||
"sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
|
||||
"sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
|
||||
],
|
||||
"version": "==2020.4.4"
|
||||
"version": "==2020.7.14"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"termcolor": {
|
||||
"hashes": [
|
||||
"sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
"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:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
|
||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
@@ -715,18 +645,12 @@
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.7.4.2"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
|
||||
"sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
|
||||
],
|
||||
"version": "==0.1.9"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,72 @@
|
||||
LNbits
|
||||
======
|
||||
|
||||
[![github-actions-badge]][github-actions]
|
||||
[![codecov-badge]][codecov]
|
||||
[![license-badge]](LICENSE)
|
||||
|
||||

|
||||
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
|
||||
https://lnbits.com, or run your own LNbits server!
|
||||
|
||||
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
|
||||
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
|
||||
|
||||
Use [lnbits.com](https://lnbits.com), or run your own LNbits server!
|
||||
|
||||
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
|
||||
|
||||
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
|
||||
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
|
||||
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
|
||||
* Part of a development stack via LNbits API
|
||||
* Fallback wallet for the LNURL scheme
|
||||
* Instant wallet for LN demonstrations
|
||||
* Instant wallet for LN demonstrations
|
||||
|
||||
The wallet can run on top of any lightning-network funding source, currently there is support for LND, CLightning, Lntxbot, LNpay, OpenNode, with more being added regularily.
|
||||
The wallet can run on top of any lightning-network funding source, currently there is support for LND, CLightning, Lntxbot, LNpay, OpenNode, with more being added regularily.
|
||||
|
||||
See [lnbits.org](https://lnbits.org) for more detailed documentation.
|
||||
|
||||
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
||||
|
||||
LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits!
|
||||
|
||||
## LNbits as an account system
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come..
|
||||
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come..
|
||||
|
||||

|
||||
|
||||
Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||
Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||
|
||||
(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats)
|
||||
(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats)
|
||||
|
||||

|
||||
|
||||
## LNbits as an LNURL-withdraw fallback
|
||||
LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the LNURL-withdraw.
|
||||
|
||||
LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the [LNURL-withdraw](https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-withdraw.md).
|
||||
|
||||

|
||||
https://github.com/btcontract/lnurl-rfc/blob/master/spec.md
|
||||
|
||||
Adding **/lnurlwallet?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
|
||||
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
|
||||
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds.
|
||||
|
||||

|
||||
|
||||
## LNbits as an insta-wallet
|
||||
|
||||
Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon).
|
||||
"Go to this website", has a lot less friction than "Download this app".
|
||||
|
||||

|
||||
|
||||
# Running LNbits locally
|
||||
Download this repo
|
||||
## Running LNbits locally
|
||||
|
||||
LNbits uses [Flask](http://flask.pocoo.org/).
|
||||
Feel free to contribute to the project.
|
||||
See the [development docs](docs/devs/installation.md) for details on installation and setup.
|
||||
|
||||
Application dependencies
|
||||
------------------------
|
||||
The application uses [Pipenv][pipenv] to manage Python packages.
|
||||
While in development, you will need to install all dependencies:
|
||||
## Tip me
|
||||
|
||||
$ pipenv shell
|
||||
$ pipenv install --dev
|
||||
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||
|
||||
You will need to set the variables in .env.example, and rename the file to .env
|
||||
|
||||

|
||||
|
||||
Running the server
|
||||
------------------
|
||||
|
||||
$ flask migrate
|
||||
$ flask run
|
||||
|
||||
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
|
||||
|
||||
[pipenv]: https://docs.pipenv.org/#install-pipenv-today
|
||||
|
||||
# Tip me
|
||||
If you like this project and might even use or extend it, why not send some tip love!
|
||||
https://paywall.link/to/f4e4e
|
||||
[github-actions]: https://github.com/lnbits/lnbits/actions
|
||||
[github-actions-badge]: https://github.com/lnbits/lnbits/workflows/test%20suite/badge.svg
|
||||
[codecov]: https://codecov.io/gh/lnbits/lnbits
|
||||
[codecov-badge]: https://codecov.io/gh/lnbits/lnbits/branch/master/graph/badge.svg
|
||||
[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
_site
|
||||
.sass-cache
|
||||
.jekyll-cache
|
||||
.jekyll-metadata
|
||||
vendor
|
||||
@@ -0,0 +1 @@
|
||||
lnbits.org
|
||||
@@ -0,0 +1,11 @@
|
||||
title: "LNbits docs"
|
||||
|
||||
remote_theme: pmarsceill/just-the-docs
|
||||
logo: "/logos/lnbits-full.png"
|
||||
search_enabled: true
|
||||
url: https://lnbits.org
|
||||
aux_links:
|
||||
"LNbits on GitHub":
|
||||
- "//github.com/lnbits/lnbits"
|
||||
"lnbits.com":
|
||||
- "//lnbits.com"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: default
|
||||
parent: For developers
|
||||
title: API reference
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
|
||||
API reference
|
||||
=============
|
||||
|
||||
Coming soon...
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: default
|
||||
title: For developers
|
||||
nav_order: 3
|
||||
has_children: true
|
||||
---
|
||||
|
||||
|
||||
For developers
|
||||
==============
|
||||
|
||||
Thanks for contributing :)
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
layout: default
|
||||
parent: For developers
|
||||
title: Making extensions
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
Making extensions
|
||||
=================
|
||||
|
||||
Start off by copying the example extension in `lnbits/extensions/example` into your own:
|
||||
```sh
|
||||
cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use dashes or anything; it doesn't like those.
|
||||
cd lnbits/extensions/mysuperplugin
|
||||
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
|
||||
```
|
||||
|
||||
Going over the example extension's structure:
|
||||
* views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.
|
||||
* views.py: The `/` path will show up as your plugin's home page in lnbits' UI. Other pages you can define yourself. The `templates` folder should explain itself in relation to this.
|
||||
* migrations.py: Create database tables for your plugin. They'll be created when you run `pipenv run flask migrate`.
|
||||
|
||||
... This document is a work-in-progress. Send pull requests if you get stuck, so others don't.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
layout: default
|
||||
parent: For developers
|
||||
title: Installation
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Download this repo and install the dependencies.
|
||||
|
||||
Application dependencies
|
||||
------------------------
|
||||
|
||||
The application uses [Pipenv][pipenv] to manage Python packages.
|
||||
While in development, you will need to install all dependencies:
|
||||
|
||||
```sh
|
||||
$ pipenv shell
|
||||
$ pipenv install --dev
|
||||
```
|
||||
If you wish to use a version of Python higher than 3.7:
|
||||
```sh
|
||||
$ pipenv --python 3.8 install --dev
|
||||
```
|
||||
You will need to set the variables in `.env.example`, and rename the file to `.env`.
|
||||
|
||||

|
||||
|
||||
You might also need to install additional packages, depending on the [backend wallets](../guide/wallets.md) you configured.
|
||||
E.g. when you want to use LND you have to `pipenv install lnd-grpc`.
|
||||
|
||||
Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
|
||||
|
||||
Running the server
|
||||
------------------
|
||||
|
||||
LNbits uses [Flask](http://flask.pocoo.org/) as an application server.
|
||||
|
||||
```sh
|
||||
$ pipenv run flask migrate
|
||||
$ pipenv run flask run
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
[pipenv]: https://pipenv.pypa.io/
|
||||
|
||||
Frontend
|
||||
--------
|
||||
|
||||
The views are build using [Vue.js and Quasar](https://quasar.dev/start/how-to-use-vue).
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
layout: default
|
||||
title: Backend wallets
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
Backend wallets
|
||||
===============
|
||||
|
||||
LNbits can run on top of many lightning-network funding sources. Currently there is support for
|
||||
CLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularily.
|
||||
|
||||
A backend wallet can be configured using the following LNbits environment variables:
|
||||
|
||||
|
||||
### CLightning
|
||||
|
||||
Using this wallet requires the installation of the `pylightning` Python package.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
|
||||
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
|
||||
|
||||
### LND (gRPC)
|
||||
|
||||
Using this wallet requires the installation of the `lnd-grpc` Python package.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
|
||||
- `LND_GRPC_ENDPOINT`: ip_address
|
||||
- `LND_GRPC_PORT`: port
|
||||
- `LND_CERT`: /file/path/tls.cert
|
||||
- `LND_ADMIN_MACAROON`: /file/path/admin.macaroon
|
||||
- `LND_INVOICE_MACAROON`: /file/path/invoice.macaroon
|
||||
- `LND_READ_MACAROON`: /file/path/read.macaroon
|
||||
|
||||
|
||||
### LND (REST)
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
|
||||
- `LND_REST_ENDPOINT`: ip_address
|
||||
- `LND_CERT`: /file/path/tls.cert
|
||||
- `LND_ADMIN_MACAROON`: /file/path/admin.macaroon
|
||||
- `LND_INVOICE_MACAROON`: /file/path/invoice.macaroon
|
||||
- `LND_READ_MACAROON`: /file/path/read.macaroon
|
||||
|
||||
|
||||
### LNbits
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
|
||||
- `LNBITS_ENDPOINT`: ex. https://lnbits.com
|
||||
- `LNBITS_ADMIN_KEY`: apiKey
|
||||
- `LNBITS_INVOICE_KEY`: apiKey
|
||||
|
||||
|
||||
### LNPay
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
|
||||
- `LNPAY_API_ENDPOINT`: ex. https://lnpay.co/v1/
|
||||
- `LNPAY_API_KEY`: apiKey
|
||||
- `LNPAY_ADMIN_KEY`: apiKey
|
||||
- `LNPAY_INVOICE_KEY`: apiKey
|
||||
- `LNPAY_READ_KEY`: apiKey
|
||||
|
||||
|
||||
### lntxbot
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LntxbotWallet**
|
||||
- `LNTXBOT_API_ENDPOINT`: ex. https://lntxbot.bigsun.xyz/
|
||||
- `LNTXBOT_ADMIN_KEY`: apiKey
|
||||
- `LNTXBOT_INVOICE_KEY`: apiKey
|
||||
|
||||
|
||||
### OpenNode
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
||||
- `OPENNODE_API_ENDPOINT`: ex. https://api.opennode.com/
|
||||
- `OPENNODE_ADMIN_KEY`: apiKey
|
||||
- `OPENNODE_INVOICE_KEY`: apiKey
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
layout: default
|
||||
title: User’s Guide
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
|
||||
LNbits, free and open-source lightning-network wallet/accounts system
|
||||
=====================================================================
|
||||
|
||||
LNbits is a very simple Python application that sits on top of any funding source, and can be used as:
|
||||
|
||||
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
|
||||
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
|
||||
* Part of a development stack via LNbits API
|
||||
* Fallback wallet for the LNURL scheme
|
||||
* Instant wallet for LN demonstrations
|
||||
|
||||
|
||||
LNbits as an account system
|
||||
---------------------------
|
||||
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending,
|
||||
export to csv + more to come...
|
||||
+2
-4
@@ -3,6 +3,7 @@ import importlib
|
||||
from flask import Flask
|
||||
from flask_assets import Environment, Bundle # type: ignore
|
||||
from flask_compress import Compress # type: ignore
|
||||
from flask_cors import CORS # type: ignore
|
||||
from flask_talisman import Talisman # type: ignore
|
||||
from os import getenv
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
@@ -23,6 +24,7 @@ valid_extensions = [ext for ext in ExtensionManager(disabled=disabled_extensions
|
||||
# -----------------------
|
||||
|
||||
Compress(app)
|
||||
CORS(app)
|
||||
Talisman(
|
||||
app,
|
||||
force_https=FORCE_HTTPS,
|
||||
@@ -33,10 +35,6 @@ Talisman(
|
||||
"'unsafe-inline'",
|
||||
"blob:",
|
||||
"api.opennode.co",
|
||||
"fonts.googleapis.com",
|
||||
"fonts.gstatic.com",
|
||||
"github.com",
|
||||
"avatars2.githubusercontent.com",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
+9
-4
@@ -114,17 +114,22 @@ def get_wallet(wallet_id: str) -> Optional[Wallet]:
|
||||
|
||||
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
|
||||
with open_db() as db:
|
||||
check_field = "adminkey" if key_type == "admin" else "inkey"
|
||||
row = db.fetchone(
|
||||
f"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE {check_field} = ?
|
||||
WHERE adminkey = ? OR inkey = ?
|
||||
""",
|
||||
(key,),
|
||||
(key, key),
|
||||
)
|
||||
|
||||
return Wallet(**row) if row else None
|
||||
if not row:
|
||||
return None
|
||||
|
||||
if key_type == "admin" and row["adminkey"] != key:
|
||||
return None
|
||||
|
||||
return Wallet(**row)
|
||||
|
||||
|
||||
# wallet payments
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin]
|
||||
});
|
||||
})
|
||||
|
||||
@@ -3,23 +3,23 @@ new Vue({
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
disclaimerDialog: {
|
||||
disclaimerDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
},
|
||||
walletName: ''
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createWallet: function () {
|
||||
LNbits.href.createWallet(this.walletName);
|
||||
LNbits.href.createWallet(this.walletName)
|
||||
},
|
||||
processing: function () {
|
||||
this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Processing...',
|
||||
icon: null
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
+200
-142
@@ -1,39 +1,49 @@
|
||||
Vue.component(VueQrcode.name, VueQrcode);
|
||||
Vue.use(VueQrcodeReader);
|
||||
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
|
||||
function generateChart(canvas, payments) {
|
||||
var txs = [];
|
||||
var n = 0;
|
||||
var txs = []
|
||||
var n = 0
|
||||
var data = {
|
||||
labels: [],
|
||||
income: [],
|
||||
outcome: [],
|
||||
cumulative: []
|
||||
};
|
||||
}
|
||||
|
||||
_.each(payments.slice(0).sort(function (a, b) {
|
||||
return a.time - b.time;
|
||||
}), function (tx) {
|
||||
txs.push({
|
||||
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
|
||||
sat: tx.sat,
|
||||
});
|
||||
});
|
||||
_.each(
|
||||
payments.slice(0).sort(function (a, b) {
|
||||
return a.time - b.time
|
||||
}),
|
||||
function (tx) {
|
||||
txs.push({
|
||||
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
|
||||
sat: tx.sat
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
_.each(_.groupBy(txs, 'hour'), function (value, day) {
|
||||
var income = _.reduce(value, function(memo, tx) {
|
||||
return (tx.sat >= 0) ? memo + tx.sat : memo;
|
||||
}, 0);
|
||||
var outcome = _.reduce(value, function(memo, tx) {
|
||||
return (tx.sat < 0) ? memo + Math.abs(tx.sat) : memo;
|
||||
}, 0);
|
||||
n = n + income - outcome;
|
||||
data.labels.push(day);
|
||||
data.income.push(income);
|
||||
data.outcome.push(outcome);
|
||||
data.cumulative.push(n);
|
||||
});
|
||||
var income = _.reduce(
|
||||
value,
|
||||
function (memo, tx) {
|
||||
return tx.sat >= 0 ? memo + tx.sat : memo
|
||||
},
|
||||
0
|
||||
)
|
||||
var outcome = _.reduce(
|
||||
value,
|
||||
function (memo, tx) {
|
||||
return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo
|
||||
},
|
||||
0
|
||||
)
|
||||
n = n + income - outcome
|
||||
data.labels.push(day)
|
||||
data.income.push(income)
|
||||
data.outcome.push(outcome)
|
||||
data.cumulative.push(n)
|
||||
})
|
||||
|
||||
new Chart(canvas.getContext('2d'), {
|
||||
type: 'bar',
|
||||
@@ -44,7 +54,7 @@ function generateChart(canvas, payments) {
|
||||
data: data.cumulative,
|
||||
type: 'line',
|
||||
label: 'balance',
|
||||
backgroundColor: '#673ab7', // deep-purple
|
||||
backgroundColor: '#673ab7', // deep-purple
|
||||
borderColor: '#673ab7',
|
||||
borderWidth: 4,
|
||||
pointRadius: 3,
|
||||
@@ -55,14 +65,14 @@ function generateChart(canvas, payments) {
|
||||
type: 'bar',
|
||||
label: 'in',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
|
||||
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
|
||||
},
|
||||
{
|
||||
data: data.outcome,
|
||||
type: 'bar',
|
||||
label: 'out',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
|
||||
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -72,18 +82,20 @@ function generateChart(canvas, payments) {
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect:false
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
display: true,
|
||||
offset: true,
|
||||
time: {
|
||||
minUnit: 'hour',
|
||||
stepSize: 3
|
||||
xAxes: [
|
||||
{
|
||||
type: 'time',
|
||||
display: true,
|
||||
offset: true,
|
||||
time: {
|
||||
minUnit: 'hour',
|
||||
stepSize: 3
|
||||
}
|
||||
}
|
||||
}],
|
||||
]
|
||||
},
|
||||
// performance tweaks
|
||||
animation: {
|
||||
@@ -95,10 +107,9 @@ function generateChart(canvas, payments) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
@@ -128,8 +139,20 @@ new Vue({
|
||||
paymentsTable: {
|
||||
columns: [
|
||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
|
||||
{name: 'sat', align: 'right', label: 'Amount (sat)', field: 'sat', sortable: true}
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: 'Date',
|
||||
field: 'date',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'sat',
|
||||
align: 'right',
|
||||
label: 'Amount (sat)',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
@@ -143,46 +166,50 @@ new Vue({
|
||||
show: false,
|
||||
location: window.location
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPayments: function () {
|
||||
var q = this.paymentsTable.filter;
|
||||
if (!q || q == '') return this.payments;
|
||||
var q = this.paymentsTable.filter
|
||||
if (!q || q == '') return this.payments
|
||||
|
||||
return LNbits.utils.search(this.payments, q);
|
||||
return LNbits.utils.search(this.payments, q)
|
||||
},
|
||||
balance: function () {
|
||||
if (this.payments.length) {
|
||||
return _.pluck(this.payments, 'amount').reduce(function (a, b) { return a + b; }, 0) / 1000;
|
||||
return (
|
||||
_.pluck(this.payments, 'amount').reduce(function (a, b) {
|
||||
return a + b
|
||||
}, 0) / 1000
|
||||
)
|
||||
}
|
||||
return this.g.wallet.sat;
|
||||
return this.g.wallet.sat
|
||||
},
|
||||
fbalance: function () {
|
||||
return LNbits.utils.formatSat(this.balance)
|
||||
},
|
||||
canPay: function () {
|
||||
if (!this.send.invoice) return false;
|
||||
return this.send.invoice.sat <= this.balance;
|
||||
if (!this.send.invoice) return false
|
||||
return this.send.invoice.sat <= this.balance
|
||||
},
|
||||
pendingPaymentsExist: function () {
|
||||
return (this.payments)
|
||||
return this.payments
|
||||
? _.where(this.payments, {pending: 1}).length > 0
|
||||
: false;
|
||||
: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeCamera: function () {
|
||||
this.sendCamera.show = false;
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
showCamera: function () {
|
||||
this.sendCamera.show = true;
|
||||
this.sendCamera.show = true
|
||||
},
|
||||
showChart: function () {
|
||||
this.paymentsChart.show = true;
|
||||
this.paymentsChart.show = true
|
||||
this.$nextTick(function () {
|
||||
generateChart(this.$refs.canvas, this.payments);
|
||||
});
|
||||
generateChart(this.$refs.canvas, this.payments)
|
||||
})
|
||||
},
|
||||
showReceiveDialog: function () {
|
||||
this.receive = {
|
||||
@@ -194,7 +221,7 @@ new Vue({
|
||||
memo: ''
|
||||
},
|
||||
paymentChecker: null
|
||||
};
|
||||
}
|
||||
},
|
||||
showSendDialog: function () {
|
||||
this.send = {
|
||||
@@ -204,52 +231,64 @@ new Vue({
|
||||
bolt11: ''
|
||||
},
|
||||
paymentChecker: null
|
||||
};
|
||||
}
|
||||
},
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker;
|
||||
var checker = this.receive.paymentChecker
|
||||
setTimeout(function () {
|
||||
clearInterval(checker);
|
||||
}, 10000);
|
||||
clearInterval(checker)
|
||||
}, 10000)
|
||||
},
|
||||
closeSendDialog: function () {
|
||||
this.sendCamera.show = false;
|
||||
var checker = this.send.paymentChecker;
|
||||
this.sendCamera.show = false
|
||||
var checker = this.send.paymentChecker
|
||||
setTimeout(function () {
|
||||
clearInterval(checker);
|
||||
}, 1000);
|
||||
clearInterval(checker)
|
||||
}, 1000)
|
||||
},
|
||||
createInvoice: function () {
|
||||
var self = this;
|
||||
this.receive.status = 'loading';
|
||||
LNbits.api.createInvoice(this.g.wallet, this.receive.data.amount, this.receive.data.memo)
|
||||
var self = this
|
||||
this.receive.status = 'loading'
|
||||
LNbits.api
|
||||
.createInvoice(
|
||||
this.g.wallet,
|
||||
this.receive.data.amount,
|
||||
this.receive.data.memo
|
||||
)
|
||||
.then(function (response) {
|
||||
self.receive.status = 'success';
|
||||
self.receive.paymentReq = response.data.payment_request;
|
||||
self.receive.status = 'success'
|
||||
self.receive.paymentReq = response.data.payment_request
|
||||
|
||||
self.receive.paymentChecker = setInterval(function () {
|
||||
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (response) {
|
||||
if (response.data.paid) {
|
||||
self.fetchPayments();
|
||||
self.receive.show = false;
|
||||
clearInterval(self.receive.paymentChecker);
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
self.receive.status = 'pending';
|
||||
});
|
||||
LNbits.api
|
||||
.getPayment(self.g.wallet, response.data.checking_id)
|
||||
.then(function (response) {
|
||||
if (response.data.paid) {
|
||||
self.fetchPayments()
|
||||
self.receive.show = false
|
||||
clearInterval(self.receive.paymentChecker)
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
self.receive.status = 'pending'
|
||||
})
|
||||
},
|
||||
decodeQR: function (res) {
|
||||
this.send.data.bolt11 = res;
|
||||
this.decodeInvoice();
|
||||
this.sendCamera.show = false;
|
||||
this.send.data.bolt11 = res
|
||||
this.decodeInvoice()
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
decodeInvoice: function () {
|
||||
if (this.send.data.bolt11.startsWith('lightning:')) {
|
||||
this.send.data.bolt11 = this.send.data.bolt11.slice(10)
|
||||
}
|
||||
|
||||
let invoice
|
||||
try {
|
||||
var invoice = decode(this.send.data.bolt11);
|
||||
invoice = decode(this.send.data.bolt11)
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 3000,
|
||||
@@ -257,101 +296,120 @@ new Vue({
|
||||
message: error + '.',
|
||||
caption: '400 BAD REQUEST',
|
||||
icon: null
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var cleanInvoice = {
|
||||
let cleanInvoice = {
|
||||
msat: invoice.human_readable_part.amount,
|
||||
sat: invoice.human_readable_part.amount / 1000,
|
||||
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
_.each(invoice.data.tags, function (tag) {
|
||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
if (tag.description == 'payment_hash') { cleanInvoice.hash = tag.value; }
|
||||
else if (tag.description == 'description') { cleanInvoice.description = tag.value; }
|
||||
else if (tag.description == 'expiry') {
|
||||
var expireDate = new Date((invoice.data.time_stamp + tag.value) * 1000);
|
||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(expireDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ');
|
||||
cleanInvoice.expired = false; // TODO
|
||||
if (tag.description == 'payment_hash') {
|
||||
cleanInvoice.hash = tag.value
|
||||
} else if (tag.description == 'description') {
|
||||
cleanInvoice.description = tag.value
|
||||
} else if (tag.description == 'expiry') {
|
||||
var expireDate = new Date(
|
||||
(invoice.data.time_stamp + tag.value) * 1000
|
||||
)
|
||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
expireDate,
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
)
|
||||
cleanInvoice.expired = false // TODO
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
this.send.invoice = Object.freeze(cleanInvoice);
|
||||
this.send.invoice = Object.freeze(cleanInvoice)
|
||||
},
|
||||
payInvoice: function () {
|
||||
var self = this;
|
||||
var self = this
|
||||
|
||||
dismissPaymentMsg = this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Processing payment...',
|
||||
icon: null
|
||||
});
|
||||
})
|
||||
|
||||
LNbits.api.payInvoice(this.g.wallet, this.send.data.bolt11).then(function (response) {
|
||||
self.send.paymentChecker = setInterval(function () {
|
||||
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (res) {
|
||||
if (res.data.paid) {
|
||||
self.send.show = false;
|
||||
clearInterval(self.send.paymentChecker);
|
||||
dismissPaymentMsg();
|
||||
self.fetchPayments();
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}).catch(function (error) {
|
||||
dismissPaymentMsg();
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
LNbits.api
|
||||
.payInvoice(this.g.wallet, this.send.data.bolt11)
|
||||
.then(function (response) {
|
||||
self.send.paymentChecker = setInterval(function () {
|
||||
LNbits.api
|
||||
.getPayment(self.g.wallet, response.data.checking_id)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
self.send.show = false
|
||||
clearInterval(self.send.paymentChecker)
|
||||
dismissPaymentMsg()
|
||||
self.fetchPayments()
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
dismissPaymentMsg()
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteWallet: function (walletId, user) {
|
||||
LNbits.utils.confirmDialog(
|
||||
'Are you sure you want to delete this wallet?'
|
||||
).onOk(function () {
|
||||
LNbits.href.deleteWallet(walletId, user);
|
||||
});
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||
.onOk(function () {
|
||||
LNbits.href.deleteWallet(walletId, user)
|
||||
})
|
||||
},
|
||||
fetchPayments: function (checkPending) {
|
||||
var self = this;
|
||||
var self = this
|
||||
|
||||
return LNbits.api.getPayments(this.g.wallet, checkPending).then(function (response) {
|
||||
self.payments = response.data.map(function (obj) {
|
||||
return LNbits.map.payment(obj);
|
||||
}).sort(function (a, b) {
|
||||
return b.time - a.time;
|
||||
});
|
||||
});
|
||||
return LNbits.api
|
||||
.getPayments(this.g.wallet, checkPending)
|
||||
.then(function (response) {
|
||||
self.payments = response.data
|
||||
.map(function (obj) {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return b.time - a.time
|
||||
})
|
||||
})
|
||||
},
|
||||
checkPendingPayments: function () {
|
||||
var dismissMsg = this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Checking pending transactions...',
|
||||
icon: null
|
||||
});
|
||||
})
|
||||
|
||||
this.fetchPayments(true).then(function () {
|
||||
dismissMsg();
|
||||
});
|
||||
dismissMsg()
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments);
|
||||
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'payments': function () {
|
||||
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance]);
|
||||
payments: function () {
|
||||
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance])
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.fetchPayments();
|
||||
setTimeout(this.checkPendingPayments(), 1200);
|
||||
this.fetchPayments()
|
||||
setTimeout(this.checkPendingPayments(), 1200)
|
||||
},
|
||||
mounted: function () {
|
||||
if (this.$refs.disclaimer && !this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
|
||||
this.disclaimerDialog.show = true;
|
||||
this.$q.localStorage.set('lnbits.disclaimerShown', true);
|
||||
if (
|
||||
this.$refs.disclaimer &&
|
||||
!this.$q.localStorage.getItem('lnbits.disclaimerShown')
|
||||
) {
|
||||
this.disclaimerDialog.show = true
|
||||
this.$q.localStorage.set('lnbits.disclaimerShown', true)
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn outline
|
||||
color="grey"
|
||||
type="a" href="https://github.com/arcbtc/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
|
||||
type="a" href="https://github.com/lnbits/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
|
||||
<q-btn outline
|
||||
color="grey"
|
||||
type="a" href="https://paywall.link/to/f4e4e" target="_blank" rel="noopener">Donate</q-btn>
|
||||
type="a" href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK" target="_blank" rel="noopener">Donate</q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.1.1/vue-qrcode-reader.min.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user, wallet) }}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
|
||||
'vendor/moment@2.24.0/moment.min.js',
|
||||
'vendor/moment@2.25.1/moment.min.js',
|
||||
'vendor/chart.js@2.9.3/chart.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% assets filters='rjsmin', output='__bundle__/core/wallet.js',
|
||||
'vendor/bolt11/utils.js',
|
||||
'vendor/bolt11/decoder.js',
|
||||
'vendor/vue-qrcode-reader@2.1.1/vue-qrcode-reader.min.js',
|
||||
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js',
|
||||
'core/js/wallet.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
|
||||
+13
-13
@@ -1,8 +1,8 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from ..services import create_invoice, pay_invoice
|
||||
@@ -20,7 +20,7 @@ def api_payments():
|
||||
else:
|
||||
payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending)
|
||||
|
||||
return jsonify(g.wallet.get_payments()), Status.OK
|
||||
return jsonify(g.wallet.get_payments()), HTTPStatus.OK
|
||||
|
||||
|
||||
@api_check_wallet_key("invoice")
|
||||
@@ -36,9 +36,9 @@ def api_payments_create_invoice():
|
||||
wallet_id=g.wallet.id, amount=g.data["amount"], memo=g.data["memo"]
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.CREATED
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@api_check_wallet_key("admin")
|
||||
@@ -47,13 +47,13 @@ def api_payments_pay_invoice():
|
||||
try:
|
||||
checking_id = pay_invoice(wallet_id=g.wallet.id, bolt11=g.data["bolt11"])
|
||||
except ValueError as e:
|
||||
return jsonify({"message": str(e)}), Status.BAD_REQUEST
|
||||
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
|
||||
except PermissionError as e:
|
||||
return jsonify({"message": str(e)}), Status.FORBIDDEN
|
||||
return jsonify({"message": str(e)}), HTTPStatus.FORBIDDEN
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id}), Status.CREATED
|
||||
return jsonify({"checking_id": checking_id}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@core_app.route("/api/v1/payments", methods=["POST"])
|
||||
@@ -70,9 +70,9 @@ def api_payment(checking_id):
|
||||
payment = g.wallet.get_payment(checking_id)
|
||||
|
||||
if not payment:
|
||||
return jsonify({"message": "Payment does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
|
||||
elif not payment.pending:
|
||||
return jsonify({"paid": True}), Status.OK
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
try:
|
||||
if payment.is_out:
|
||||
@@ -80,10 +80,10 @@ def api_payment(checking_id):
|
||||
elif payment.is_in:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
if is_paid:
|
||||
payment.set_pending(False)
|
||||
return jsonify({"paid": True}), Status.OK
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
|
||||
from os import path
|
||||
from http import HTTPStatus
|
||||
from os import getenv, path
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.settings import SERVICE_FEE
|
||||
|
||||
from ..crud import (
|
||||
@@ -33,7 +33,7 @@ def extensions():
|
||||
extension_to_disable = request.args.get("disable", type=str)
|
||||
|
||||
if extension_to_enable and extension_to_disable:
|
||||
abort(Status.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
|
||||
abort(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
|
||||
|
||||
if extension_to_enable:
|
||||
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
|
||||
@@ -60,7 +60,11 @@ def wallet():
|
||||
if not user_id:
|
||||
user = get_user(create_account().id)
|
||||
else:
|
||||
user = get_user(user_id) or abort(Status.NOT_FOUND, "User does not exist.")
|
||||
user = get_user(user_id) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
|
||||
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
|
||||
|
||||
if allowed_users != "all" and user_id not in allowed_users.split(","):
|
||||
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.")
|
||||
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name:
|
||||
@@ -71,7 +75,7 @@ def wallet():
|
||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||
|
||||
if wallet_id not in user.wallet_ids:
|
||||
abort(Status.FORBIDDEN, "Not your wallet.")
|
||||
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
|
||||
return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee)
|
||||
|
||||
@@ -84,7 +88,7 @@ def deletewallet():
|
||||
user_wallet_ids = g.user.wallet_ids
|
||||
|
||||
if wallet_id not in user_wallet_ids:
|
||||
abort(Status.FORBIDDEN, "Not your wallet.")
|
||||
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
else:
|
||||
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
|
||||
user_wallet_ids.remove(wallet_id)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import requests
|
||||
|
||||
from flask 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
|
||||
from time import sleep
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from ..crud import create_account, get_user, create_wallet, create_payment
|
||||
@@ -17,9 +17,13 @@ def lnurlwallet():
|
||||
memo = "LNbits LNURL funding"
|
||||
|
||||
try:
|
||||
withdraw_res = handle_lnurl(request.args.get("lightning"), response_class=LnurlWithdrawResponse)
|
||||
withdraw_res = handle_lnurl(request.args.get("lightning"))
|
||||
if not withdraw_res.ok:
|
||||
abort(HTTPStatus.BAD_REQUEST, f"Could not process LNURL-withdraw: {withdraw_res.error_msg}")
|
||||
if not isinstance(withdraw_res, LnurlWithdrawResponse):
|
||||
abort(HTTPStatus.BAD_REQUEST, "Not a valid LNURL-withdraw.")
|
||||
except LnurlException:
|
||||
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.")
|
||||
|
||||
try:
|
||||
ok, checking_id, payment_request, error_message = WALLET.create_invoice(withdraw_res.max_sats, memo)
|
||||
@@ -27,7 +31,7 @@ def lnurlwallet():
|
||||
ok, error_message = False, str(e)
|
||||
|
||||
if not ok:
|
||||
abort(Status.INTERNAL_SERVER_ERROR, error_message)
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, error_message)
|
||||
|
||||
r = requests.get(
|
||||
withdraw_res.callback.base,
|
||||
@@ -35,7 +39,7 @@ def lnurlwallet():
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.")
|
||||
|
||||
for i in range(10):
|
||||
invoice_status = WALLET.get_invoice_status(checking_id)
|
||||
|
||||
+15
-9
@@ -1,11 +1,12 @@
|
||||
from cerberus import Validator # type: ignore
|
||||
from flask import g, abort, jsonify, request
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
from os import getenv
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from .helpers import Status
|
||||
|
||||
|
||||
def api_check_wallet_key(key_type: str = "invoice"):
|
||||
@@ -15,10 +16,10 @@ def api_check_wallet_key(key_type: str = "invoice"):
|
||||
try:
|
||||
g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type)
|
||||
except KeyError:
|
||||
return jsonify({"message": "`X-Api-Key` header missing."}), Status.BAD_REQUEST
|
||||
return jsonify({"message": "`X-Api-Key` header missing."}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
if not g.wallet:
|
||||
return jsonify({"message": "Wrong keys."}), Status.UNAUTHORIZED
|
||||
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
@@ -32,13 +33,13 @@ def api_validate_post_request(*, schema: dict):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
if "application/json" not in request.headers["Content-Type"]:
|
||||
return jsonify({"message": "Content-Type must be `application/json`."}), Status.BAD_REQUEST
|
||||
return jsonify({"message": "Content-Type must be `application/json`."}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
v = Validator(schema)
|
||||
g.data = {key: request.json[key] for key in schema.keys()}
|
||||
g.data = {key: (request.json[key] if key in request.json else None) for key in schema.keys()}
|
||||
|
||||
if not v.validate(g.data):
|
||||
return jsonify({"message": f"Errors in request data: {v.errors}"}), Status.BAD_REQUEST
|
||||
return jsonify({"message": f"Errors in request data: {v.errors}"}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
@@ -51,7 +52,12 @@ def check_user_exists(param: str = "usr"):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
g.user = get_user(request.args.get(param, type=str)) or abort(Status.NOT_FOUND, "User not found.")
|
||||
g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
|
||||
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
|
||||
|
||||
if allowed_users != "all" and g.user.id not in allowed_users.split(","):
|
||||
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
@@ -67,13 +73,13 @@ def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = Fals
|
||||
|
||||
for param, value in query_params.items():
|
||||
if not value and (required is True or (required and param in required)):
|
||||
abort(Status.BAD_REQUEST, f"`{param}` is required.")
|
||||
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.")
|
||||
|
||||
if value:
|
||||
try:
|
||||
UUID(value, version=version)
|
||||
except ValueError:
|
||||
abort(Status.BAD_REQUEST, f"`{param}` is not a valid UUID.")
|
||||
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
|
||||
<p>Milking faucets with software, known as "assmilking", seems at first to be black-hat, although in fact there might be some unexplored use cases. An LNURL withdraw gives someone the right to pull funds, which can be done over time. An LNURL withdraw could be used outside of just faucets, to provide money streaming and repeat payments.<br/>Paste or scan an LNURL withdraw, enter the amount for the AMilk to pull and the frequency for it to be pulled.<br/>
|
||||
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
|
||||
</q-card>
|
||||
</q-card-section>
|
||||
|
||||
</q-card-section></q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
|
||||
<p>
|
||||
Milking faucets with software, known as "assmilking", seems at first to
|
||||
be black-hat, although in fact there might be some unexplored use cases.
|
||||
An LNURL withdraw gives someone the right to pull funds, which can be
|
||||
done over time. An LNURL withdraw could be used outside of just faucets,
|
||||
to provide money streaming and repeat payments.<br />Paste or scan an
|
||||
LNURL withdraw, enter the amount for the AMilk to pull and the frequency
|
||||
for it to be pulled.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -1,230 +1,252 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true"
|
||||
>New AMilk</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true">New AMilk</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">AMilks</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">AMilks</h5>
|
||||
</div>
|
||||
<q-table dense flat
|
||||
:data="amilks"
|
||||
row-key="id"
|
||||
:columns="amilksTable.columns"
|
||||
:pagination.sync="amilksTable.pagination">
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
|
||||
>
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn flat dense size="xs" @click="deleteAMilk(props.row.id)" icon="cancel" color="pink"></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits Assistant Faucet Milker Extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "amilk/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="amilkDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="createAMilk" class="q-gutter-md">
|
||||
<q-select filled dense emit-value v-model="amilkDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
|
||||
</q-select>
|
||||
<q-input filled dense
|
||||
v-model.trim="amilkDialog.data.lnurl"
|
||||
type="url"
|
||||
label="LNURL Withdraw"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.number="amilkDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.trim="amilkDialog.data.atime"
|
||||
type="number"
|
||||
label="Hit frequency (secs)"
|
||||
placeholder="Frequency to be hit"></q-input>
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
|
||||
type="submit">Create amilk</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="amilks"
|
||||
row-key="id"
|
||||
:columns="amilksTable.columns"
|
||||
:pagination.sync="amilksTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteAMilk(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
<script>
|
||||
var mapAMilk = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
|
||||
obj.wall = ['/amilk/', obj.id].join('');
|
||||
return obj;
|
||||
}
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
LNbits Assistant Faucet Milker Extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "amilk/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
amilks: [],
|
||||
amilksTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
|
||||
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
|
||||
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
amilkDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
<q-dialog v-model="amilkDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="createAMilk" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="amilkDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="amilkDialog.data.lnurl"
|
||||
type="url"
|
||||
label="LNURL Withdraw"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="amilkDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount *"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="amilkDialog.data.atime"
|
||||
type="number"
|
||||
label="Hit frequency (secs)"
|
||||
placeholder="Frequency to be hit"
|
||||
></q-input>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
|
||||
type="submit"
|
||||
>Create amilk</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapAMilk = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.wall = ['/amilk/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
amilks: [],
|
||||
amilksTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
|
||||
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
|
||||
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
amilkDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAMilks: function () {
|
||||
var self = this
|
||||
|
||||
|
||||
|
||||
getAMilks: function () {
|
||||
var self = this;
|
||||
|
||||
LNbits.api.request(
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/amilk/api/v1/amilk?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
).then(function (response) {
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks = response.data.map(function (obj) {
|
||||
response.data.forEach(MILK);
|
||||
function MILK(item){
|
||||
|
||||
window.setInterval(function(){
|
||||
|
||||
|
||||
LNbits.api.request(
|
||||
'GET',
|
||||
'/amilk/api/v1/amilk/milk/' + item.id,
|
||||
"Lorem"
|
||||
).then(function (response) {
|
||||
self.amilks = response.data.map(function (obj) {
|
||||
return mapAMilk(obj);
|
||||
});
|
||||
});
|
||||
},
|
||||
item.atime*1000);
|
||||
|
||||
response.data.forEach(MILK)
|
||||
function MILK(item) {
|
||||
window.setInterval(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/amilk/api/v1/amilk/milk/' + item.id,
|
||||
'Lorem'
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks = response.data.map(function (obj) {
|
||||
return mapAMilk(obj)
|
||||
})
|
||||
})
|
||||
}, item.atime * 1000)
|
||||
}
|
||||
return mapAMilk(obj);
|
||||
});
|
||||
});
|
||||
},
|
||||
createAMilk: function () {
|
||||
var data = {
|
||||
lnurl: this.amilkDialog.data.lnurl,
|
||||
atime: parseInt(this.amilkDialog.data.atime),
|
||||
amount: this.amilkDialog.data.amount
|
||||
};
|
||||
var self = this;
|
||||
return mapAMilk(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
createAMilk: function () {
|
||||
var data = {
|
||||
lnurl: this.amilkDialog.data.lnurl,
|
||||
atime: parseInt(this.amilkDialog.data.atime),
|
||||
amount: this.amilkDialog.data.amount
|
||||
}
|
||||
var self = this
|
||||
|
||||
console.log(this.amilkDialog.data.wallet);
|
||||
console.log(this.amilkDialog.data.wallet)
|
||||
|
||||
LNbits.api.request(
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/amilk/api/v1/amilk',
|
||||
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet}).inkey,
|
||||
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet})
|
||||
.inkey,
|
||||
data
|
||||
).then(function (response) {
|
||||
self.amilks.push(mapAMilk(response.data));
|
||||
self.amilkDialog.show = false;
|
||||
self.amilkDialog.data = {};
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
},
|
||||
deleteAMilk: function (amilkId) {
|
||||
var self = this;
|
||||
var amilk = _.findWhere(this.amilks, {id: amilkId});
|
||||
|
||||
LNbits.utils.confirmDialog(
|
||||
'Are you sure you want to delete this AMilk link?'
|
||||
).onOk(function () {
|
||||
LNbits.api.request(
|
||||
'DELETE',
|
||||
'/amilk/api/v1/amilks/' + amilkId,
|
||||
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
|
||||
).then(function (response) {
|
||||
self.amilks = _.reject(self.amilks, function (obj) { return obj.id == amilkId; });
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks);
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks.push(mapAMilk(response.data))
|
||||
self.amilkDialog.show = false
|
||||
self.amilkDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getAMilks();
|
||||
}
|
||||
deleteAMilk: function (amilkId) {
|
||||
var self = this
|
||||
var amilk = _.findWhere(this.amilks, {id: amilkId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this AMilk link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/amilk/api/v1/amilks/' + amilkId,
|
||||
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks = _.reject(self.amilks, function (obj) {
|
||||
return obj.id == amilkId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getAMilks()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from flask import g, abort, render_template
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.amilk import amilk_ext
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from .crud import get_amilk
|
||||
|
||||
@@ -16,6 +16,6 @@ def index():
|
||||
|
||||
@amilk_ext.route("/<amilk_id>")
|
||||
def wall(amilk_id):
|
||||
amilk = get_amilk(amilk_id) or abort(Status.NOT_FOUND, "AMilk does not exist.")
|
||||
amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.")
|
||||
|
||||
return render_template("amilk/wall.html", amilk=amilk)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from lnbits.extensions.amilk import amilk_ext
|
||||
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
|
||||
@@ -23,7 +23,7 @@ def api_amilks():
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), Status.OK
|
||||
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
|
||||
@@ -34,10 +34,10 @@ def api_amilkit(amilk_id):
|
||||
try:
|
||||
withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse)
|
||||
except LnurlException:
|
||||
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
print(withdraw_res.max_sats)
|
||||
|
||||
|
||||
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo)
|
||||
#print(payment_request)
|
||||
@@ -48,10 +48,10 @@ def api_amilkit(amilk_id):
|
||||
withdraw_res.callback.base,
|
||||
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}},
|
||||
)
|
||||
|
||||
|
||||
if not r.ok:
|
||||
|
||||
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
|
||||
for i in range(10):
|
||||
invoice_status = WALLET.get_invoice_status(checking_id)
|
||||
@@ -59,10 +59,10 @@ def api_amilkit(amilk_id):
|
||||
if not invoice_status.paid:
|
||||
continue
|
||||
else:
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
break
|
||||
|
||||
return jsonify({"paid": True}), Status.OK
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
|
||||
@amilk_ext.route("/api/v1/amilk", methods=["POST"])
|
||||
@@ -75,7 +75,7 @@ def api_amilkit(amilk_id):
|
||||
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()), Status.CREATED
|
||||
return jsonify(amilk._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
|
||||
@@ -84,11 +84,11 @@ def api_amilk_delete(amilk_id):
|
||||
amilk = get_amilk(amilk_id)
|
||||
|
||||
if not amilk:
|
||||
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if amilk.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your amilk."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_amilk(amilk_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
"name": "Diagon Alley",
|
||||
"short_description": "Movable anonymous market stand",
|
||||
"icon": "add_shopping_cart",
|
||||
"contributors": ["eillarra"]
|
||||
"contributors": ["benarc"]
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">Diagon Alley: Movable, anonymous market stall</h5>
|
||||
<p>Make a list of products to sell, point your list of products at a public indexer. Buyers browse your products on the indexer, and pay you directly. Ratings are managed by the indexer. Your stall can be listed in multiple indexers, even over TOR, if you wish to be anonymous.<br/>
|
||||
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
|
||||
</q-card>
|
||||
</q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Diagon Alley: Decentralised Market-Stalls
|
||||
</h5>
|
||||
<p>
|
||||
Make a list of products to sell, point your list of products at a public
|
||||
indexer. Buyers browse your products on the indexer, and pay you
|
||||
directly. Ratings are managed by the indexer. Your stall can be listed
|
||||
in multiple indexers, even over TOR, if you wish to be anonymous.<br />
|
||||
More information on the
|
||||
<a href="https://github.com/lnbits/Diagon-Alley"
|
||||
>Diagon Alley Protocol</a
|
||||
><br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
@@ -21,43 +31,91 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
|
||||
<q-expansion-item group="api" dense expand-separator label="Get prodcuts, categorised by wallet">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get prodcuts, categorised by wallet"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-blue">GET</span> /api/v1/diagonalley/stall/products/<indexer_id></code>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/api/v1/diagonalley/stall/products/<indexer_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>Product JSON list</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X GET {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/products/<indexer_id></code>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}diagonalley/api/v1/diagonalley/stall/products/<indexer_id></code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Get invoice for product">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get invoice for product"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-green">POST</span> /api/v1/diagonalley/stall/order/<indexer_id></code>
|
||||
<code
|
||||
><span class="text-light-green">POST</span>
|
||||
/api/v1/diagonalley/stall/order/<indexer_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"id": <string>, "address": <string>, "shippingzone": <integer>, "email": <string>, "quantity": <integer>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
|
||||
<code>{"checking_id": <string>,"payment_request": <string>}</code>
|
||||
<code
|
||||
>{"id": <string>, "address": <string>, "shippingzone":
|
||||
<integer>, "email": <string>, "quantity":
|
||||
<integer>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"checking_id": <string>,"payment_request":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X POST {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/order/<indexer_id> -d '{"id": <product_id&>, "email": <customer_email>, "address": <customer_address>, "quantity": 2, "shippingzone": 1}' -H "Content-type: application/json"
|
||||
</code>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root
|
||||
}}diagonalley/api/v1/diagonalley/stall/order/<indexer_id> -d
|
||||
'{"id": <product_id&>, "email": <customer_email>,
|
||||
"address": <customer_address>, "quantity": 2, "shippingzone":
|
||||
1}' -H "Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Check a product has been shipped"
|
||||
class="q-mb-md">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Check a product has been shipped"
|
||||
class="q-mb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-blue">GET</span> /diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id></code>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200 OK (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>{"shipped": <boolean>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X GET {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id> -H "Content-type: application/json"</code>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id>
|
||||
-H "Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1,3 @@
|
||||
<script>console.log("{{ stall }}")</script>
|
||||
<script>
|
||||
console.log('{{ stall }}')
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from flask import g, abort, render_template, jsonify
|
||||
import json
|
||||
|
||||
from flask import g, abort, render_template, jsonify
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.diagonalley import diagonalley_ext
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from lnbits.extensions.diagonalley import diagonalley_ext
|
||||
from .crud import create_diagonalleys_product,get_diagonalleys_product,get_diagonalleys_products,delete_diagonalleys_product,create_diagonalleys_indexer,update_diagonalleys_indexer,get_diagonalleys_indexer,get_diagonalleys_indexers,delete_diagonalleys_indexer,create_diagonalleys_order,get_diagonalleys_order,get_diagonalleys_orders,delete_diagonalleys_order
|
||||
@@ -21,7 +21,7 @@ def api_diagonalley_products():
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), Status.OK
|
||||
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["POST"])
|
||||
@@ -41,16 +41,16 @@ def api_diagonalley_product_create(product_id=None):
|
||||
product = get_diagonalleys_indexer(product_id)
|
||||
|
||||
if not product:
|
||||
return jsonify({"message": "Withdraw product does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Withdraw product does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if product.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw product."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your withdraw product."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
product = update_diagonalleys_product(product_id, **g.data)
|
||||
else:
|
||||
product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify(product._asdict()), Status.OK if product_id else Status.CREATED
|
||||
return jsonify(product._asdict()), HTTPStatus.OK if product_id else HTTPStatus.CREATED
|
||||
|
||||
|
||||
|
||||
@@ -60,14 +60,14 @@ def api_diagonalley_products_delete(product_id):
|
||||
product = get_diagonalleys_product(product_id)
|
||||
|
||||
if not product:
|
||||
return jsonify({"message": "Product does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Product does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if product.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Diagon Alley."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your Diagon Alley."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_product(product_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def api_diagonalley_indexers():
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), Status.OK
|
||||
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["POST"])
|
||||
@@ -102,16 +102,16 @@ def api_diagonalley_indexer_create(indexer_id=None):
|
||||
indexer = get_diagonalleys_indexer(indexer_id)
|
||||
|
||||
if not indexer:
|
||||
return jsonify({"message": "Withdraw indexer does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Withdraw indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if indexer.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw indexer."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your withdraw indexer."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
indexer = update_diagonalleys_indexer(indexer_id, **g.data)
|
||||
else:
|
||||
indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify(indexer._asdict()), Status.OK if indexer_id else Status.CREATED
|
||||
return jsonify(indexer._asdict()), HTTPStatus.OK if indexer_id else HTTPStatus.CREATED
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"])
|
||||
@@ -120,14 +120,14 @@ def api_diagonalley_indexer_delete(indexer_id):
|
||||
indexer = get_diagonalleys_indexer(indexer_id)
|
||||
|
||||
if not indexer:
|
||||
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if indexer.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_indexer(indexer_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
###Orders
|
||||
@@ -140,7 +140,7 @@ def api_diagonalley_orders():
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), Status.OK
|
||||
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["POST"])
|
||||
@@ -154,7 +154,7 @@ def api_diagonalley_orders():
|
||||
})
|
||||
def api_diagonalley_order_create():
|
||||
order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data)
|
||||
return jsonify(order._asdict()), Status.CREATED
|
||||
return jsonify(order._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders/<order_id>", methods=["DELETE"])
|
||||
@@ -163,14 +163,14 @@ def api_diagonalley_order_delete(order_id):
|
||||
order = get_diagonalleys_order(order_id)
|
||||
|
||||
if not order:
|
||||
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if order.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_indexer(order_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders/paid/<order_id>", methods=["GET"])
|
||||
@@ -178,7 +178,7 @@ def api_diagonalley_order_delete(order_id):
|
||||
def api_diagonalleys_order_paid(order_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE orders SET paid = ? WHERE id = ?", (True, order_id,))
|
||||
return "", Status.OK
|
||||
return "", HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/<order_id>", methods=["GET"])
|
||||
@@ -188,7 +188,7 @@ def api_diagonalleys_order_shipped(order_id):
|
||||
db.execute("UPDATE orders SET shipped = ? WHERE id = ?", (True, order_id,))
|
||||
order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
|
||||
|
||||
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), Status.OK
|
||||
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), HTTPStatus.OK
|
||||
|
||||
|
||||
###List products based on indexer id
|
||||
@@ -199,13 +199,13 @@ def api_diagonalleys_stall_products(indexer_id):
|
||||
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
|
||||
print(rows[1])
|
||||
if not rows:
|
||||
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
products = db.fetchone("SELECT * FROM products WHERE wallet = ?", (rows[1],))
|
||||
if not products:
|
||||
return jsonify({"message": "No products"}), Status.NOT_FOUND
|
||||
return jsonify({"message": "No products"}), HTTPStatus.NOT_FOUND
|
||||
|
||||
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), Status.OK
|
||||
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), HTTPStatus.OK
|
||||
|
||||
###Check a product has been shipped
|
||||
|
||||
@@ -214,7 +214,7 @@ def api_diagonalleys_stall_checkshipped(checking_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
|
||||
|
||||
return jsonify({"shipped": rows["shipped"]}), Status.OK
|
||||
return jsonify({"shipped": rows["shipped"]}), HTTPStatus.OK
|
||||
|
||||
###Place order
|
||||
|
||||
@@ -245,7 +245,4 @@ def api_diagonalley_stall_order(indexer_id):
|
||||
""",
|
||||
(selling_id ,g.data["id"] , product.wallet, product.product, g.data["quantity"], g.data["shippingzone"], g.data["address"], g.data["email"], checking_id, False, False),
|
||||
)
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.OK
|
||||
|
||||
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
<h1> LNEVENTS</h1>
|
||||
<h2>Create/sell tickets for an event</h2>
|
||||
<p>Events is an easy way to create/sell tickets for an event.
|
||||
|
||||
It is advised to setup a specific wallet in lnbits for the event.</p>
|
||||
|
||||
<img src="https://i.imgur.com/qHi7ExL.png">
|
||||
|
||||
<h1>Events</h1>
|
||||
<h2>Events: Sell and register event tickets</h2>
|
||||
Events alows you to make a wave of tickets for an event, each ticket is in the form of a unqiue QRcode, which the user presents at registration. Events comes with a shareable ticket scanner, which can be used to register attendees.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Events",
|
||||
"short_description": "Sell and register event tickets",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["benarc"]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Events",
|
||||
"short_description": "LN tickets for events.",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Events",
|
||||
"short_description": "LN tickets for events.",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import Tickets, Events
|
||||
|
||||
|
||||
#######TICKETS########
|
||||
|
||||
|
||||
def create_ticket(checking_id: str, wallet: str, event: str, name: str, email: str) -> Tickets:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (id, wallet, event, name, email, registered, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(checking_id, wallet, event, name, email, False, False),
|
||||
)
|
||||
|
||||
return get_ticket(checking_id)
|
||||
|
||||
def update_ticket(paid: bool, checking_id: str) -> Tickets:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
|
||||
if row[6] == True:
|
||||
return get_ticket(checking_id)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE ticket
|
||||
SET paid = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(paid, checking_id),
|
||||
)
|
||||
|
||||
eventdata = get_event(row[2])
|
||||
sold = eventdata.sold + 1
|
||||
amount_tickets = eventdata.amount_tickets - 1
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE events
|
||||
SET sold = ?, amount_tickets = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(sold, amount_tickets, row[2]),
|
||||
)
|
||||
return get_ticket(checking_id)
|
||||
|
||||
|
||||
def get_ticket(checking_id: str) -> Optional[Tickets]:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
|
||||
|
||||
return Tickets(**row) if row else None
|
||||
|
||||
|
||||
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
print("scrum")
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_ticket(checking_id: str) -> None:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("DELETE FROM ticket WHERE id = ?", (checking_id,))
|
||||
|
||||
|
||||
|
||||
########EVENTS#########
|
||||
|
||||
|
||||
def create_event(*, wallet: str, name: str, info: str, closing_date: str, event_start_date: str, event_end_date: str, amount_tickets: int, price_per_ticket: int) -> Events:
|
||||
with open_ext_db("events") as db:
|
||||
event_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(event_id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, 0),
|
||||
)
|
||||
print(event_id)
|
||||
|
||||
return get_event(event_id)
|
||||
|
||||
def update_event(event_id: str, **kwargs) -> Events:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
with open_ext_db("events") as db:
|
||||
db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id))
|
||||
|
||||
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
return Events(**row) if row else None
|
||||
|
||||
|
||||
def get_event(event_id: str) -> Optional[Events]:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
return Events(**row) if row else None
|
||||
|
||||
|
||||
def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Events(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_event(event_id: str) -> None:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("DELETE FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
########EVENTTICKETS#########
|
||||
|
||||
def get_event_tickets(event_id: str, wallet_id: str) -> Tickets:
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
rows = db.fetchall("SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id))
|
||||
print(rows)
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
def reg_ticket(ticket_id: str) -> Tickets:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("UPDATE ticket SET registered = ? WHERE id = ?", (True, ticket_id))
|
||||
ticket = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
|
||||
print(ticket[1])
|
||||
rows = db.fetchall("SELECT * FROM ticket WHERE event = ?", (ticket[1],))
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
||||
@@ -1,5 +1,93 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
def m001_initial(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
info TEXT NOT NULL,
|
||||
closing_date TEXT NOT NULL,
|
||||
event_start_date TEXT NOT NULL,
|
||||
event_end_date TEXT NOT NULL,
|
||||
amount_tickets INTEGER NOT NULL,
|
||||
price_per_ticket INTEGER NOT NULL,
|
||||
sold INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
registered BOOLEAN NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def m002_changed(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS ticket (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
registered BOOLEAN NOT NULL,
|
||||
paid BOOLEAN NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]:
|
||||
usescsv = ""
|
||||
|
||||
for i in range(row[5]):
|
||||
if row[7]:
|
||||
usescsv += "," + str(i + 1)
|
||||
else:
|
||||
usescsv += "," + str(1)
|
||||
usescsv = usescsv[1:]
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (
|
||||
id,
|
||||
wallet,
|
||||
event,
|
||||
name,
|
||||
email,
|
||||
registered,
|
||||
paid
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
row[0],
|
||||
row[1],
|
||||
row[2],
|
||||
row[3],
|
||||
row[4],
|
||||
row[5],
|
||||
True,
|
||||
),
|
||||
)
|
||||
db.execute("DROP TABLE tickets")
|
||||
|
||||
def migrate():
|
||||
print("pending")
|
||||
with open_ext_db("events") as db:
|
||||
m001_initial(db)
|
||||
m002_changed(db)
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Events(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
info: str
|
||||
closing_date: str
|
||||
event_start_date: str
|
||||
event_end_date: str
|
||||
amount_tickets: int
|
||||
price_per_ticket: int
|
||||
sold: int
|
||||
time: int
|
||||
|
||||
class Tickets(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
event: str
|
||||
name: str
|
||||
email: str
|
||||
registered: bool
|
||||
paid: bool
|
||||
time: int
|
||||
@@ -1,26 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
usr TEXT,
|
||||
wal TEXT,
|
||||
walnme TEXT,
|
||||
walinvkey INTEGER,
|
||||
uni TEXT,
|
||||
tit TEXT,
|
||||
cldate TEXT,
|
||||
notickets INTEGER,
|
||||
sold INTEGER DEFAULT 0,
|
||||
prtick INTEGER,
|
||||
descr TEXT,
|
||||
unireg TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eventssold (
|
||||
key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uni TEXT,
|
||||
email TEXT,
|
||||
name TEXT,
|
||||
hash TEXT,
|
||||
paid INTEGER DEFAULT 0,
|
||||
reg INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Events: Sell and register ticket waves for an event
|
||||
</h5>
|
||||
<p>
|
||||
Events alows you to make a wave of tickets for an event, each ticket is
|
||||
in the form of a unqiue QRcode, which the user presents at registration.
|
||||
Events comes with a shareable ticket scanner, which can be used to
|
||||
register attendees.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>
|
||||
</small>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
@@ -1,570 +1,205 @@
|
||||
<!-- @format -->
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h3 class="q-my-none">{{ event_name }}</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">{{ event_info }}</h5>
|
||||
<br />
|
||||
<q-form @submit="Invoice()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Your name "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.email"
|
||||
type="email"
|
||||
label="Your email "
|
||||
></q-input>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background:#1f2234;
|
||||
border-left-color:#8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color:
|
||||
#2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper, .right-side {
|
||||
background-color:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color:
|
||||
#1f2234;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color:
|
||||
#4b646f;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid
|
||||
transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
color: #fff;
|
||||
background:#3e355a;
|
||||
border-left-color:#8964a9;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
|
||||
background-color:
|
||||
#1f2234 !important;
|
||||
}
|
||||
.alert-danger, .alert-error {
|
||||
|
||||
border-color: #fff;
|
||||
border: 1px solid
|
||||
|
||||
#fff;
|
||||
border-radius: 7px;
|
||||
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color:
|
||||
#f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
|
||||
background-color:
|
||||
#3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
|
||||
background-color:#333646;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
.box-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
.modal-content {
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
|
||||
background-color: #333646;
|
||||
|
||||
}
|
||||
|
||||
.layout-boxed {
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
<p>{% raw %}{{amountWords}}{% endraw %}</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar" style="height: auto;">
|
||||
<q-card v-show="ticketLink.show" class="q-pa-lg">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="ticketLink.data.link" target="_blank">
|
||||
<q-btn unelevated size="xl" color="deep-purple"
|
||||
>Link to your ticket!</q-btn
|
||||
></a
|
||||
>
|
||||
<br /><br />
|
||||
<p>You'll be redirected in a few moments...</p>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
LNBits Events
|
||||
<small>Lightning powered tickets</small>
|
||||
</h1>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
console.log('{{ form_costpword }}')
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content"><br/><br/>
|
||||
<center><h1 style="font-size:500%">{{ nme }}</h1></center>
|
||||
<center><h2 style="width:55%;word-wrap: break-word;" >{{ descr }}</h2></center>
|
||||
<div id="theform">
|
||||
<br/><br/><br/>
|
||||
<center>
|
||||
|
||||
<form role="form">
|
||||
|
||||
<div class="form-group" style="width:300px;">
|
||||
|
||||
<input id="Nam" type="text" class="form-control" placeholder="Name"></input>
|
||||
<input id="Ema" type="text" class="form-control" placeholder="Email"></input>
|
||||
</div>
|
||||
<button onclick="submitforticket()" type="button" class="btn btn-info">Go to payment</button><p style="color:red;" id="error"></p>
|
||||
</form>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
<center><br/><br/> <div id="qrcode" style="width: 340px;"></div><br/><br/>
|
||||
<div style="width:55%;word-wrap: break-word;" id="qrcodetxt"></div> <br/></center>
|
||||
|
||||
</section><!-- /.content -->
|
||||
</div><!-- /.content-wrapper -->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function postAjax(url, data, thekey, success) {
|
||||
var params =
|
||||
typeof data == 'string'
|
||||
? data
|
||||
: Object.keys(data)
|
||||
.map(function(k) {
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
|
||||
})
|
||||
.join('&')
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('POST', url)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.send(params)
|
||||
return xhr
|
||||
}
|
||||
|
||||
function getAjax(url, thekey, success) {
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('GET', url, true)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
|
||||
xhr.send()
|
||||
return xhr
|
||||
}
|
||||
|
||||
function submitforticket(){
|
||||
|
||||
nam = document.getElementById('Nam').value
|
||||
ema = document.getElementById('Ema').value
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('events.api_getticket') }}?ema=" + ema,
|
||||
JSON.stringify({"unireg": "{{wave }}", "name": nam}),
|
||||
"filla",
|
||||
|
||||
function(data) {
|
||||
theinvoice = JSON.parse(data).pay_req
|
||||
thehash = JSON.parse(data).payment_hash
|
||||
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: theinvoice,
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
})
|
||||
document.getElementById('theform').innerHTML = ""
|
||||
|
||||
document.getElementById("qrcode").style.backgroundColor = "white";
|
||||
document.getElementById("qrcode").style.padding = "20px";
|
||||
|
||||
|
||||
document.getElementById('qrcodetxt').innerHTML = theinvoice + "<br/><br/>"
|
||||
|
||||
var refreshId = setInterval(function(){
|
||||
|
||||
getAjax('/api/v1/invoice/' + thehash, "{{wave}}", function(datab) {
|
||||
console.log(JSON.parse(datab).PAID)
|
||||
if (JSON.parse(datab).PAID == 'TRUE') {
|
||||
location.replace("{{ url_for('events.ticket') }}?hash="+thehash + "&unireg={{wave}}")
|
||||
clearInterval(refreshId)
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
name: '',
|
||||
email: ''
|
||||
}
|
||||
},
|
||||
ticketLink: {
|
||||
show: false,
|
||||
data: {
|
||||
link: ''
|
||||
}
|
||||
},
|
||||
receive: {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null
|
||||
}
|
||||
})}, 3000);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetForm: function (e) {
|
||||
e.preventDefault()
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
},
|
||||
|
||||
})
|
||||
}
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker
|
||||
dismissMsg()
|
||||
|
||||
clearInterval(paymentChecker)
|
||||
setTimeout(function () {}, 10000)
|
||||
},
|
||||
Invoice: function () {
|
||||
var self = this
|
||||
axios
|
||||
|
||||
.post(
|
||||
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
|
||||
{
|
||||
event: '{{ event_id }}',
|
||||
event_name: '{{ event_name }}',
|
||||
name: self.formDialog.data.name,
|
||||
email: self.formDialog.data.email
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.checking_id
|
||||
|
||||
</script>
|
||||
</html>
|
||||
dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
|
||||
self.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: self.paymentReq
|
||||
}
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios
|
||||
.get('/events/api/v1/tickets/' + self.paymentCheck)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
dismissMsg()
|
||||
self.formDialog.data.name = ''
|
||||
self.formDialog.data.email = ''
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: null
|
||||
})
|
||||
self.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
|
||||
self.ticketLink = {
|
||||
show: true,
|
||||
data: {
|
||||
link: '/events/ticket/' + res.data.ticket_id
|
||||
}
|
||||
}
|
||||
setTimeout(function () {
|
||||
window.location.href =
|
||||
'/events/ticket/' + res.data.ticket_id
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ event_name }} error</h3>
|
||||
<br />
|
||||
<q-icon
|
||||
name="warning"
|
||||
class="text-grey"
|
||||
style="font-size: 20rem;"
|
||||
></q-icon>
|
||||
|
||||
<h5 class="q-my-none">{{ event_error }}</h5>
|
||||
<br />
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
@@ -1,480 +1,509 @@
|
||||
<!-- @format -->
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
>New Event</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<span class="label label-danger">!</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="header"><b>Instant wallet, bookmark to save</b></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block menuitems %}
|
||||
<li class="treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-bitcoin"></i> <span>Wallets</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for w in user_wallets %}
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"><i class="fa fa-bolt"></i> {{ w.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><a onclick="sidebarmake()">Add a wallet +</a></li>
|
||||
<div id="sidebarmake"></div>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="active treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-th"></i> <span>Extensions</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for extension in EXTENSIONS %}
|
||||
{% if extension.code in user_ext %}
|
||||
<li>
|
||||
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"><i class="fa fa-plus"></i> {{ extension.name }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Events
|
||||
<small>bitcoin tickets</small>
|
||||
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">Lightning tickets</i>
|
||||
</li>
|
||||
</ol>
|
||||
<br /><br />
|
||||
</section>
|
||||
<style>
|
||||
.datepicker-days{
|
||||
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
</style>
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<!-- Small boxes (Stat box) -->
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- general form elements -->
|
||||
<div class="box box-primary">
|
||||
|
||||
|
||||
<div class="box-header">
|
||||
<h3 class="box-title"> Make a ticket wave</h3>
|
||||
</div><!-- /.box-header -->
|
||||
|
||||
|
||||
<!-- form start -->
|
||||
<form role="form">
|
||||
<div class="box-body">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Ticket title</label>
|
||||
<input id="tit" type="text" pattern="^[A-Za-z]+$" class="form-control" >
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Description of event</label>
|
||||
<textarea id="descr" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- select -->
|
||||
<div class="form-group">
|
||||
<label>Select a wallet</label>
|
||||
<select id="wal" class="form-control">
|
||||
<option></option>
|
||||
{% for w in user_wallets %}
|
||||
<option>{{w.name}}-{{w.id}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nooftickets">No. of tickets</label>
|
||||
<input id="notickets" type="number" class="form-control" placeholder="10" max="86400"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Close date:</label>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" id="datepicker"></input>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="prpertick">Price per ticket</label>
|
||||
<input id="prtickets" type="number" class="form-control" placeholder="10"></input>
|
||||
</div>
|
||||
|
||||
</div><!-- /.box-body -->
|
||||
|
||||
<div class="box-footer">
|
||||
|
||||
<button onclick="postev()" type="button" class="btn btn-info">Create Wave</button><p style="color:red;" id="error"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div></div></div>
|
||||
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- general form elements -->
|
||||
<div class="box box-primary">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Select a link</h3>
|
||||
</div><!-- /.box-header -->
|
||||
<form role="form">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
|
||||
<select class="form-control" id="waveselect" onchange="drawwithdraw()">
|
||||
<option value="none" selected>
|
||||
Select an Option
|
||||
</option>
|
||||
{% for w in user_ev %}
|
||||
<option id="{{w.uni}}" value="{{w.tit}}-{{w.unireg}}-{{w.uni}}">{{w.tit}}-{{w.unireg}}-{{w.uni}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<center> <br/><div id="qrcode" style="width:340px" ></div><br/><div style="width:75%;word-wrap: break-word;" id="qrcodetxt" ></div></center>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div><!-- /.box -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Ticket Waves<b id="withdraws"></b></h3>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Events</h5>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body no-padding">
|
||||
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th style="width:15%">Amt</th>
|
||||
<th style="width:15%">Sold</th>
|
||||
<th style="width:15%">Closing</th>
|
||||
<th style="width:15%">Price</th>
|
||||
<th style="width:15%">Wallet</th>
|
||||
<th style="width:10%">Edit</th>
|
||||
<th style="width:10%">Del</th>
|
||||
</tr>
|
||||
<tbody id="ticketwaves"></tbody>
|
||||
</table>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exporteventsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="events"
|
||||
row-key="id"
|
||||
:columns="eventsTable.columns"
|
||||
:pagination.sync="eventsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="link"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="how_to_reg"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/register/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateformDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteEvent(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<div id="editlink"></div>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportticketsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="local_activity"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/ticket/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<!-- /.content -->
|
||||
</section>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
|
||||
<script>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTicket(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits Events extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "events/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
//Date picker
|
||||
$('#datepicker').datepicker({
|
||||
autoclose: true
|
||||
})
|
||||
|
||||
|
||||
|
||||
window.user = {{ user | tojson | safe }}
|
||||
window.user_wallets = {{ user_wallets | tojson | safe }}
|
||||
window.user_ext = {{ user_ext | tojson | safe }}
|
||||
window.user_ev = {{ user_ev | tojson | safe }}
|
||||
|
||||
const user_ev = window.user_ev
|
||||
console.log(user_ev)
|
||||
|
||||
|
||||
function drawChart(user_ev) {
|
||||
var transactionsHTML = ''
|
||||
|
||||
for (var i = 0; i < user_ev.length; i++) {
|
||||
var ev = user_ev[i]
|
||||
|
||||
transactionsHTML =
|
||||
"<tr><td style='width: 50%'>" +
|
||||
ev.tit +
|
||||
'</td><td>' +
|
||||
ev.notickets +
|
||||
'</td><td>' +
|
||||
ev.sold +
|
||||
'</td><td>' +
|
||||
ev.cldate +
|
||||
'</td><td>' +
|
||||
ev.prtick +
|
||||
'</td><td>' +
|
||||
"<a href='{{ url_for('wallet') }}?usr="+ ev.usr +"'>" + ev.wal.substring(0, 4) + "...</a>" +
|
||||
'</td><td>' +
|
||||
"<i onclick='editlink("+ i +")'' class='fa fa-edit'></i>" +
|
||||
'</td><td>' +
|
||||
"<b><a style='color:red;' href='" + "{{ url_for('events.index') }}?del=" + ev.uni + "&usr=" + ev.usr +"'>" + "<i class='fa fa-trash'></i>" + "</a></b>" +
|
||||
'</td></tr>' +
|
||||
transactionsHTML
|
||||
document.getElementById('ticketwaves').innerHTML = transactionsHTML
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (user_ev.length) {
|
||||
drawChart(user_ev)
|
||||
}
|
||||
|
||||
function postev(){
|
||||
|
||||
wal = document.getElementById('wal').value
|
||||
tit = document.getElementById('tit').value
|
||||
cldate = document.getElementById('datepicker').value
|
||||
notickets = document.getElementById('notickets').value
|
||||
prtickets = document.getElementById('prtickets').value
|
||||
descr = document.getElementById('descr').value
|
||||
|
||||
if (tit == "") {
|
||||
document.getElementById("error").innerHTML = "Only use letters in title"
|
||||
return amt
|
||||
}
|
||||
if (wal == "") {
|
||||
document.getElementById("error").innerHTML = "No wallet selected"
|
||||
return amt
|
||||
}
|
||||
if (cldate == "") {
|
||||
document.getElementById("error").innerHTML = "No date selected"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(notickets) || notickets < 1) {
|
||||
document.getElementById("error").innerHTML = "Must be more than 1"
|
||||
return amt
|
||||
}
|
||||
if (isNaN(prtickets) || prtickets < 10) {
|
||||
document.getElementById("error").innerHTML = "Must be higher 10"
|
||||
return amt
|
||||
}
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('events.create') }}",
|
||||
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": notickets,"cldate": cldate, "prtickets": prtickets, "descr": descr}),
|
||||
"filla",
|
||||
|
||||
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
function editlink(evnum){
|
||||
|
||||
evdetails = user_ev[evnum]
|
||||
|
||||
console.log(evdetails.descr)
|
||||
wallpick = ""
|
||||
|
||||
checkbox = ""
|
||||
if (evdetails.uniq == 1){
|
||||
checkbox = "checked"}
|
||||
|
||||
document.getElementById('editlink').innerHTML = "<div class='row'>"+
|
||||
"<div class='col-md-6'>"+
|
||||
" <!-- general form elements -->"+
|
||||
"<div class='box box-primary' style='min-height: 300px;'>"+
|
||||
"<div class='box-header'>"+
|
||||
"<h3 class='box-title'> Edit: <i id='unid'>" + evdetails.tit + "-" + evdetails.uni + "-" + evdetails.unireg + "</i> </h3>"+
|
||||
"<div class='box-tools pull-right'>" +
|
||||
"<button class='btn btn-box-tool' data-widget='remove'><i class='fa fa-times'></i></button>" +
|
||||
"</div>" +
|
||||
" </div><!-- /.box-header -->"+
|
||||
" <!-- form start -->"+
|
||||
"<form role='form'>"+
|
||||
"<div class='box-body'>"+
|
||||
|
||||
"<div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Link title</label>"+
|
||||
"<input id='edittit' type='text' class='form-control' value='"+
|
||||
evdetails.tit +
|
||||
"'></input> </div>"+
|
||||
" </div>"+
|
||||
|
||||
"<div class='col-sm-1 col-md-8'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Description of event</label>"+
|
||||
"<textarea id='editdescr' type='textarea' rows='1' class='form-control'>"+evdetails.descr+"</textarea> </div>"+
|
||||
" </div>"+
|
||||
|
||||
|
||||
" <div class='col-sm-4 col-md-4'>"+
|
||||
" <!-- select -->"+
|
||||
" <div class='form-group'>"+
|
||||
" <label>Select a wallet</label>"+
|
||||
"<select id='editwal' class='form-control'>"+
|
||||
" <option>" + evdetails.walnme + "-" + evdetails.wal + "</option>"+
|
||||
" {% for w in user_wallets %}"+
|
||||
|
||||
" <option>{{w.name}}-{{w.id}}</option>"+
|
||||
|
||||
" {% endfor %}"+
|
||||
" </select>"+
|
||||
" </div>"+
|
||||
" </div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
" <label for='exampleInputPassword1'>No of tickets:</label>"+
|
||||
" <input id='editnooftickets' type='number' class='form-control' placeholder='0' max='86400' value='"+
|
||||
evdetails.notickets +
|
||||
"'></input>"+
|
||||
"</div> </div>"+
|
||||
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Price per ticket:</label>"+
|
||||
" <input id='editprtick' type='number' class='form-control' placeholder='1' value='"+
|
||||
evdetails.prtick +
|
||||
"'></input>"+
|
||||
" </div></div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
" <div class='input-group date'>"+
|
||||
" <label for='exampleInputEmail1'>Close date:</label>"+
|
||||
" <input id='datepicker2' type='text' class='form-control' placeholder='1' value='"+
|
||||
evdetails.cldate +
|
||||
"'></input>"+
|
||||
" </div></div>"+
|
||||
|
||||
|
||||
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"</div><!-- /.box-body -->"+
|
||||
" </div><br/>"+
|
||||
" <div class='box-footer'>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<button onclick='editlinkcont()' type='button' style='margin: 24px;' class='btn btn-info'>Edit link(s)</button>"+
|
||||
"</div>"+
|
||||
"<p style='color:red;' id='error2'>.</p>"+
|
||||
" </div></form></div><!-- /.box --></div></div>"
|
||||
|
||||
|
||||
}
|
||||
|
||||
//Date picker
|
||||
$('#datepicker2').datepicker({
|
||||
autoclose: true
|
||||
})
|
||||
|
||||
function editlinkcont(){
|
||||
|
||||
unid = document.getElementById('unid').innerHTML
|
||||
wal = document.getElementById('editwal').value
|
||||
tit = document.getElementById('edittit').value
|
||||
nooftickets = document.getElementById('editnooftickets').value
|
||||
prtick = document.getElementById('editprtick').value
|
||||
cldate = document.getElementById('datepicker2').value
|
||||
descr = document.getElementById('editdescr').value
|
||||
uni = unid.split("-")[1]
|
||||
|
||||
if (tit == "") {
|
||||
document.getElementById("error2").innerHTML = "Only use letters in title"
|
||||
return amt
|
||||
}
|
||||
if (wal == "") {
|
||||
document.getElementById("error2").innerHTML = "No wallet selected"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(nooftickets) || nooftickets < 1 || nooftickets > 1000000) {
|
||||
document.getElementById("error2").innerHTML = "No. of tickets must be between 1 - 1000000"
|
||||
return amt
|
||||
}
|
||||
if (isNaN(prtick) || prtick < 10 ) {
|
||||
document.getElementById("error2").innerHTML = "Ticket pricket must be higher than 10"
|
||||
return amt
|
||||
}
|
||||
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('events.create') }}",
|
||||
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": nooftickets,"cldate": cldate, "prtickets": prtick, "id": unid, "descr": descr}),
|
||||
|
||||
"filla",
|
||||
|
||||
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
//draws withdraw QR code
|
||||
function drawwithdraw() {
|
||||
|
||||
document.getElementById("qrcode").innerHTML = "";
|
||||
walname = document.getElementById("waveselect").value
|
||||
|
||||
thewave = walname.split("-");
|
||||
console.log(window.location.hostname + "-" + thewave[1])
|
||||
toencode = "/events/wave/" + thewave[1]
|
||||
toreg = "/events/registration/" + thewave[2]
|
||||
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: toencode,
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
})
|
||||
|
||||
|
||||
document.getElementById('qrcodetxt').innerHTML = "<a href='" + toencode + "'><h2>Payment link</a> , " +
|
||||
"<a href='" + toreg + "'>Registration</h2></a>"
|
||||
|
||||
document.getElementById("qrcode").style.backgroundColor = "white";
|
||||
document.getElementById("qrcode").style.padding = "20px";
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendEventData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Name of event "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.info"
|
||||
type="textarea"
|
||||
label="Info about the event "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.closing_date"
|
||||
type="date"
|
||||
label="Closing for tickets "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_start_date"
|
||||
type="date"
|
||||
label="Event begins "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_end_date"
|
||||
type="date"
|
||||
label="Event ends "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.amount_tickets"
|
||||
type="number"
|
||||
label="Amount of tickets "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.price_per_ticket"
|
||||
type="number"
|
||||
label="Price per ticket "
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update Event</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.wallet == null || formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || formDialog.data.price_per_ticket == null"
|
||||
type="submit"
|
||||
>Create Event</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapEvents = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
events: [],
|
||||
tickets: [],
|
||||
eventsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'info', align: 'left', label: 'Info', field: 'info'},
|
||||
{
|
||||
name: 'event_start_date',
|
||||
align: 'left',
|
||||
label: 'Start date',
|
||||
field: 'event_start_date'
|
||||
},
|
||||
{
|
||||
name: 'event_end_date',
|
||||
align: 'left',
|
||||
label: 'End date',
|
||||
field: 'event_end_date'
|
||||
},
|
||||
{
|
||||
name: 'closing_date',
|
||||
align: 'left',
|
||||
label: 'Ticket close',
|
||||
field: 'closing_date'
|
||||
},
|
||||
{
|
||||
name: 'price_per_ticket',
|
||||
align: 'left',
|
||||
label: 'Price',
|
||||
field: 'price_per_ticket'
|
||||
},
|
||||
{
|
||||
name: 'amount_tickets',
|
||||
align: 'left',
|
||||
label: 'No tickets',
|
||||
field: 'amount_tickets'
|
||||
},
|
||||
{
|
||||
name: 'sold',
|
||||
align: 'left',
|
||||
label: 'Sold',
|
||||
field: 'sold'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'event', align: 'left', label: 'Event', field: 'event'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
||||
{
|
||||
name: 'registered',
|
||||
align: 'left',
|
||||
label: 'Registered',
|
||||
field: 'registered'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTickets: function () {
|
||||
var self = this
|
||||
console.log('obj')
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/tickets?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
console.log(obj)
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteTicket: function (ticketId) {
|
||||
var self = this
|
||||
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this ticket')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/events/api/v1/tickets/' + ticketId,
|
||||
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = _.reject(self.tickets, function (obj) {
|
||||
return obj.id == ticketId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportticketsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||
},
|
||||
|
||||
getEvents: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/events?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = response.data.map(function (obj) {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
sendEventData: function () {
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
var data = this.formDialog.data
|
||||
|
||||
if (data.id) {
|
||||
this.updateEvent(wallet, data)
|
||||
} else {
|
||||
this.createEvent(wallet, data)
|
||||
}
|
||||
},
|
||||
|
||||
createEvent: function (wallet, data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request('POST', '/events/api/v1/events', wallet.inkey, data)
|
||||
.then(function (response) {
|
||||
self.events.push(mapEvents(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.events, {id: formId})
|
||||
console.log(link.id)
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
this.formDialog.data.name = link.name
|
||||
this.formDialog.data.info = link.info
|
||||
this.formDialog.data.closing_date = link.closing_date
|
||||
this.formDialog.data.event_start_date = link.event_start_date
|
||||
this.formDialog.data.event_end_date = link.event_end_date
|
||||
this.formDialog.data.amount_tickets = link.amount_tickets
|
||||
this.formDialog.data.price_per_ticket = link.price_per_ticket
|
||||
this.formDialog.show = true
|
||||
},
|
||||
updateEvent: function (wallet, data) {
|
||||
var self = this
|
||||
console.log(data)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/events/api/v1/events/' + data.id,
|
||||
wallet.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = _.reject(self.events, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.events.push(mapEvents(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteEvent: function (eventsId) {
|
||||
var self = this
|
||||
var events = _.findWhere(this.events, {id: eventsId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this form link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/events/api/v1/events/' + eventsId,
|
||||
_.findWhere(self.g.user.wallets, {id: events.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = _.reject(self.events, function (obj) {
|
||||
return obj.id == eventsId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exporteventsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.eventsTable.columns, this.events)
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTickets()
|
||||
this.getEvents()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ event_name }} Registration</h3>
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
<q-btn unelevated color="deep-purple" @click="showCamera" size="xl"
|
||||
>Scan ticket</q-btn
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="local_activity"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/ticket/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="sendCamera.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl">
|
||||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream
|
||||
@decode="decodeQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block styles %}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}"
|
||||
/>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
|
||||
'vendor/moment@2.25.1/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% 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' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
var mapEvents = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tickets: [],
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{
|
||||
name: 'registered',
|
||||
align: 'left',
|
||||
label: 'Registered',
|
||||
field: 'registered'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
sendCamera: {
|
||||
show: false,
|
||||
camera: 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hoverEmail: function (tmp) {
|
||||
this.tickets.data.emailtemp = tmp
|
||||
},
|
||||
closeCamera: function () {
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
showCamera: function () {
|
||||
this.sendCamera.show = true
|
||||
},
|
||||
decodeQR: function (res) {
|
||||
this.sendCamera.show = false
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request('GET', '/events/api/v1/register/ticket/' + res)
|
||||
.then(function (response) {
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Registered!'
|
||||
})
|
||||
setTimeout(function () {
|
||||
window.location.reload()
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getEventTickets: function () {
|
||||
var self = this
|
||||
console.log('obj')
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.getEventTickets()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,690 +0,0 @@
|
||||
<!-- @format -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background:#1f2234;
|
||||
border-left-color:#8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color:
|
||||
#2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper, .right-side {
|
||||
background-color:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color:
|
||||
#1f2234;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color:
|
||||
#4b646f;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid
|
||||
transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
color: #fff;
|
||||
background:#3e355a;
|
||||
border-left-color:#8964a9;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
|
||||
background-color:
|
||||
#1f2234 !important;
|
||||
}
|
||||
.alert-danger, .alert-error {
|
||||
|
||||
border-color: #fff;
|
||||
border: 1px solid
|
||||
|
||||
#fff;
|
||||
border-radius: 7px;
|
||||
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color:
|
||||
#f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
|
||||
background-color:
|
||||
#3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
|
||||
background-color:#333646;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
.box-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
.modal-content {
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
|
||||
background-color: #333646;
|
||||
|
||||
}
|
||||
|
||||
.layout-boxed {
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar" style="height: auto;">
|
||||
<!-- Sidebar user panel -->
|
||||
|
||||
|
||||
</section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
LNBits Events
|
||||
<small>Lightning powered tickets</small>
|
||||
</h1>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content"><br/><br/>
|
||||
<center><h1 style="font-size:500%">{{ user_ev[0][6] }}</h1></center>
|
||||
|
||||
|
||||
<br/><br/><br/>
|
||||
|
||||
<div class='modal fade sends' tabindex='-1' role='dialog' aria-labelledby='myLargeModalLabel' aria-hidden='true'>
|
||||
<div class='modal-dialog' >
|
||||
<div id='scantickets' style='padding: 0 10px 0 10px;'>
|
||||
</div></div></div>
|
||||
|
||||
|
||||
<center>
|
||||
<button
|
||||
onclick="scanQRsend()"
|
||||
class="btn btn-block btn-primary btn-lg"
|
||||
data-toggle="modal"
|
||||
data-target=".sends"
|
||||
style="width:300px;"
|
||||
>
|
||||
Scan ticket
|
||||
</button>
|
||||
</center>
|
||||
|
||||
|
||||
<div id="scantickets"></div>
|
||||
<br/><br/><br/>
|
||||
<div id="qrcodetxt"></div> <br/></center>
|
||||
<br/><br/><br/>
|
||||
|
||||
<center>
|
||||
<div class="row" style="width:80%;margin-top:80px">
|
||||
|
||||
|
||||
<style>
|
||||
.ema, button:focus .txt {
|
||||
display: none;
|
||||
}
|
||||
button:focus .ema {
|
||||
display: block;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Attendees<b id="withdraws"></b></h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body no-padding">
|
||||
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
|
||||
<tr>
|
||||
<th style="width:20%">Name</th>
|
||||
<th style="width:20%">Email</th>
|
||||
<th style="width:50%">Ticket</th>
|
||||
<th style="width:10%">Registered</th>
|
||||
</tr>
|
||||
<tbody id="ticketwaves"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
|
||||
</div>
|
||||
</center>
|
||||
|
||||
</section><!-- /.content -->
|
||||
</div><!-- /.content-wrapper -->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
window.user_ev = {{ user_ev | tojson | safe }}
|
||||
window.user_ev_sold = {{ user_ev_sold | tojson | safe }}
|
||||
console.log(user_ev)
|
||||
console.log(user_ev_sold)
|
||||
|
||||
|
||||
function drawChart(user_ev_sold) {
|
||||
var transactionsHTML = ''
|
||||
|
||||
for (var i = 0; i < user_ev_sold.length; i++) {
|
||||
var ev = user_ev_sold[i]
|
||||
|
||||
transactionsHTML =
|
||||
"<tr><td>" +
|
||||
ev.name +
|
||||
'</td><td>' +
|
||||
'<button style="background-color: #333646;padding: 0;border: none;background: none;" class="lost"><span class="txt">xxxxxx</span>' +
|
||||
'<span class="ema">' + ev.email + '</span></button>'+
|
||||
'</td><td>' +
|
||||
ev.hash +
|
||||
'</td><td>' +
|
||||
ev.reg +
|
||||
'</td></tr>' +
|
||||
transactionsHTML
|
||||
document.getElementById('ticketwaves').innerHTML = transactionsHTML
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (user_ev_sold.length) {
|
||||
drawChart(user_ev_sold)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function postAjax(url, data, thekey, success) {
|
||||
var params =
|
||||
typeof data == 'string'
|
||||
? data
|
||||
: Object.keys(data)
|
||||
.map(function(k) {
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
|
||||
})
|
||||
.join('&')
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('POST', url)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.send(params)
|
||||
return xhr
|
||||
}
|
||||
|
||||
function getAjax(url, thekey, success) {
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('GET', url, true)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
|
||||
xhr.send()
|
||||
return xhr
|
||||
}
|
||||
|
||||
function scanQRsend() {
|
||||
document.getElementById('scantickets').innerHTML =
|
||||
"<div class='modal-content'>"+
|
||||
"<br/><div id='registered'><div id='loadingMessage'>🎥 Unable to access video stream (please make sure you have a webcam enabled)</div>" +
|
||||
"<canvas id='canvas' hidden></canvas><div id='output' hidden><div id='outputMessage'></div>" +
|
||||
"<br/><span id='outputData'></span></div></div><div class='modal-footer'>"+
|
||||
"<button type='submit' class='btn btn-primary' onclick='cancelsend()'>Cancel</button><br/><br/></div>"
|
||||
var video = document.createElement('video')
|
||||
var canvasElement = document.getElementById('canvas')
|
||||
var canvas = canvasElement.getContext('2d')
|
||||
var loadingMessage = document.getElementById('loadingMessage')
|
||||
var outputContainer = document.getElementById('output')
|
||||
var outputMessage = document.getElementById('outputMessage')
|
||||
var outputData = document.getElementById('outputData')
|
||||
|
||||
// Use facingMode: environment to attemt to get the front camera on phones
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({video: {facingMode: 'environment'}})
|
||||
.then(function(stream) {
|
||||
video.srcObject = stream
|
||||
video.setAttribute('playsinline', true) // required to tell iOS safari we don't want fullscreen
|
||||
video.play()
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
|
||||
function tick() {
|
||||
loadingMessage.innerText = '⌛ Loading video...'
|
||||
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
||||
loadingMessage.hidden = true
|
||||
canvasElement.hidden = false
|
||||
outputContainer.hidden = false
|
||||
canvasElement.height = video.videoHeight
|
||||
canvasElement.width = video.videoWidth
|
||||
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height)
|
||||
var imageData = canvas.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvasElement.width,
|
||||
canvasElement.height
|
||||
)
|
||||
var code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: 'dontInvert'
|
||||
})
|
||||
if (code) {
|
||||
thehash = code.data
|
||||
document.getElementById('registered').innerHTML = "<h1 style='color:green;font-size:300%;'>Registered!</h1>"
|
||||
outputMessage.hidden = true
|
||||
outputData.parentElement.hidden = false
|
||||
outputData.innerText = JSON.stringify(code.data)
|
||||
getAjax("{{ url_for('events.api_checkticket') }}?thehash=" + thehash, "filla", function(datab) {
|
||||
if (JSON.parse(datab).status == 'TRUE') {
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
outputMessage.hidden = false
|
||||
outputData.parentElement.hidden = true
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
}
|
||||
}
|
||||
|
||||
function cancelsend() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
function getUrlVars() {
|
||||
var vars = {};
|
||||
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
|
||||
vars[key] = value;
|
||||
});
|
||||
return vars;
|
||||
}
|
||||
var name = getUrlVars()["name"];
|
||||
var thehash = getUrlVars()["thehash"];
|
||||
console.log(thehash)
|
||||
if(thehash != null){
|
||||
document.getElementById('qrcodetxt').innerHTML = "<center><h1>" + name + " is registered!</h1></center>"
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</html>
|
||||
@@ -1,474 +1,46 @@
|
||||
<!-- @format -->
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ ticket_name }} Ticket</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">
|
||||
Bookmark, print or screenshot this page,<br />
|
||||
and present it for registration!
|
||||
</h5>
|
||||
<br />
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background:#1f2234;
|
||||
border-left-color:#8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color:
|
||||
#2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper, .right-side {
|
||||
background-color:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color:
|
||||
#1f2234;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color:
|
||||
#4b646f;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid
|
||||
transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
color: #fff;
|
||||
background:#3e355a;
|
||||
border-left-color:#8964a9;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
|
||||
background-color:
|
||||
#1f2234 !important;
|
||||
}
|
||||
.alert-danger, .alert-error {
|
||||
|
||||
border-color: #fff;
|
||||
border: 1px solid
|
||||
|
||||
#fff;
|
||||
border-radius: 7px;
|
||||
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color:
|
||||
#f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
|
||||
background-color:
|
||||
#3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
|
||||
background-color:#333646;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
.box-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
.modal-content {
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
|
||||
background-color: #333646;
|
||||
|
||||
}
|
||||
|
||||
.layout-boxed {
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
<qrcode
|
||||
:value="'{{ ticket_id }}'"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
<br />
|
||||
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
|
||||
<q-icon left size="3em" name="print"></q-icon> Print</q-btn
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar" style="height: auto;">
|
||||
|
||||
</section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
LNBits Events
|
||||
<small>Lightning powered tickets</small>
|
||||
</h1>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content"><br/><br/>
|
||||
|
||||
<center><h2 style="width:70%;font-size:400%" >Bookmark/Screenshot this page. <br/>It is your ticket!</h2></center>
|
||||
<center> <div style="width:340px;background-color:white;padding:20px"id="qrcode"></div></center>
|
||||
|
||||
|
||||
</section><!-- /.content -->
|
||||
</div><!-- /.content-wrapper -->
|
||||
</div>
|
||||
</body>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: "{{ticket}}",
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
</html>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
printWindow: function () {
|
||||
window.print()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,174 +1,45 @@
|
||||
import uuid
|
||||
from flask import g, abort, render_template
|
||||
from datetime import date, datetime
|
||||
|
||||
from flask import jsonify, render_template, request, redirect, url_for
|
||||
from datetime import datetime
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
from lnbits.extensions.events import events_ext
|
||||
from .crud import get_ticket, get_event
|
||||
|
||||
|
||||
@events_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
"""Main events link page."""
|
||||
usr = request.args.get("usr")
|
||||
return render_template("events/index.html", user=g.user)
|
||||
|
||||
if usr:
|
||||
if not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# Get all the data
|
||||
with open_db() as db:
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
@events_ext.route("/<event_id>")
|
||||
def display(event_id):
|
||||
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
if event.amount_tickets < 1:
|
||||
return render_template("events/error.html", event_name=event.name, event_error="Sorry, tickets are sold out :(")
|
||||
datetime_object = datetime.strptime(event.closing_date, '%Y-%m-%d').date()
|
||||
if date.today() > datetime_object:
|
||||
return render_template("events/error.html", event_name=event.name, event_error="Sorry, ticket closing date has passed :(")
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
|
||||
|
||||
# If del is selected by user from events page, the event link is to be deleted
|
||||
evdel = request.args.get("del")
|
||||
if evdel:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (evdel,))
|
||||
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (evdel,))
|
||||
if user_ev[0][9] > 0:
|
||||
events_ext_db.execute("DELETE FROM eventssold WHERE uni = ?", (user_ev[0][12],))
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
|
||||
print(user_ext)
|
||||
return render_template("events/display.html", event_id=event_id, event_name=event.name, event_info=event.info, event_price=event.price_per_ticket)
|
||||
|
||||
return render_template(
|
||||
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
|
||||
)
|
||||
|
||||
@events_ext.route("/create", methods=["GET", "POST"])
|
||||
def create():
|
||||
"""."""
|
||||
@events_ext.route("/ticket/<ticket_id>")
|
||||
def ticket(ticket_id):
|
||||
ticket = get_ticket(ticket_id) or abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.")
|
||||
event = get_event(ticket.event) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
return render_template("events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info)
|
||||
|
||||
data = request.json
|
||||
tit = data["tit"]
|
||||
wal = data["wal"]
|
||||
cldate = data["cldate"]
|
||||
notickets = data["notickets"]
|
||||
prtick = data["prtickets"]
|
||||
usr = data["usr"]
|
||||
descr = data["descr"]
|
||||
wall = wal.split("-")
|
||||
|
||||
# Form validation
|
||||
if (
|
||||
not tit.replace(" ", "").isalnum()
|
||||
or wal == ""
|
||||
or int(notickets) < 0
|
||||
or int(prtick) < 0
|
||||
):
|
||||
return jsonify({"ERROR": "FORM ERROR"}), 401
|
||||
|
||||
# If id that means its a link being edited, delete the record first
|
||||
if "id" in data:
|
||||
unid = data["id"].split("-")
|
||||
uni = unid[1]
|
||||
unireg = unid[2]
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (unid[1],))
|
||||
else:
|
||||
uni = uuid.uuid4().hex
|
||||
unireg = uuid.uuid4().hex
|
||||
|
||||
@events_ext.route("/register/<event_id>")
|
||||
def register(event_id):
|
||||
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
|
||||
with open_db() as dbb:
|
||||
user_wallets = dbb.fetchall("SELECT * FROM wallets WHERE user = ? AND id = ?", (usr, wall[1],))
|
||||
if not user_wallets:
|
||||
return jsonify({"ERROR": "NO WALLET USER"}), 401
|
||||
|
||||
with open_db() as db:
|
||||
user_ext = db.fetchall("SELECT * FROM extensions WHERE user = ?", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
# Add to DB
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
events_ext_db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO events
|
||||
(usr, wal, walnme, walinvkey, uni, tit, cldate, notickets, prtick, descr, unireg)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
usr,
|
||||
wall[1],
|
||||
user_wallets[0][1],
|
||||
user_wallets[0][4],
|
||||
uni,
|
||||
tit,
|
||||
cldate,
|
||||
notickets,
|
||||
prtick,
|
||||
descr,
|
||||
unireg,
|
||||
),
|
||||
)
|
||||
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
|
||||
|
||||
if not user_ev:
|
||||
return jsonify({"ERROR": "NO WALLET USER"}), 401
|
||||
|
||||
return render_template(
|
||||
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
|
||||
)
|
||||
return render_template("events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet)
|
||||
|
||||
|
||||
|
||||
@events_ext.route("/wave/<wave>/", methods=["GET", "POST"])
|
||||
def wave(wave):
|
||||
"""."""
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (wave,))
|
||||
if not user_ev:
|
||||
return jsonify({"ERROR": "NO RECORD"}), 401
|
||||
|
||||
return render_template(
|
||||
"events/display.html", wave=wave, nme=user_ev[0][6], descr=user_ev[0][11]
|
||||
)
|
||||
|
||||
@events_ext.route("/registration/<wave>", methods=["GET", "POST"])
|
||||
def registration(wave):
|
||||
"""."""
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (wave,))
|
||||
user_ev_sold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE uni = ? AND paid = 1", (user_ev[0][12],))
|
||||
if not user_ev:
|
||||
return jsonify({"ERROR": "NO RECORD"}), 401
|
||||
|
||||
return render_template(
|
||||
"events/registration.html", user_ev=user_ev, user_ev_sold=user_ev_sold
|
||||
)
|
||||
|
||||
@events_ext.route("/ticket/", methods=["GET"])
|
||||
def ticket():
|
||||
"""."""
|
||||
thehash = request.args.get("hash")
|
||||
unireg = request.args.get("unireg")
|
||||
|
||||
#Double check the payment has cleared
|
||||
with open_db() as db:
|
||||
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (thehash,))
|
||||
|
||||
if not payment:
|
||||
return jsonify({"status": "ERROR", "reason":"NO RECORD OF PAYMENT"}), 400
|
||||
|
||||
if payment[0][4] == 1:
|
||||
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 400
|
||||
|
||||
#Update databases
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
|
||||
updatesold = user_ev[0][9] + 1
|
||||
events_ext_db.execute("UPDATE events SET sold = ? WHERE unireg = ?", (updatesold, unireg,))
|
||||
events_ext_db.execute("UPDATE eventssold SET paid = 1 WHERE hash = ?", (thehash,))
|
||||
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
|
||||
if not eventssold:
|
||||
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
|
||||
|
||||
return render_template(
|
||||
"events/ticket.html", name=eventssold[0][3], ticket=thehash
|
||||
)
|
||||
|
||||
@@ -1,65 +1,171 @@
|
||||
import uuid
|
||||
import json
|
||||
import requests
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from flask import jsonify, request, url_for
|
||||
from datetime import datetime
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
from lnbits.extensions.events import events_ext
|
||||
|
||||
@events_ext.route("/api/v1/getticket/", methods=["GET","POST"])
|
||||
def api_getticket():
|
||||
"""."""
|
||||
|
||||
data = request.json
|
||||
unireg = data["unireg"]
|
||||
name = data["name"]
|
||||
email = request.args.get("ema")
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
|
||||
from .crud import create_ticket, update_ticket, get_ticket, get_tickets, delete_ticket, create_event, update_event, get_event, get_events, delete_event, get_event_tickets, reg_ticket
|
||||
|
||||
|
||||
header = {"Content-Type": "application/json", "X-Api-Key": user_ev[0][4]}
|
||||
data = {"value": str(user_ev[0][10]), "memo": user_ev[0][6]}
|
||||
print(url_for("api_invoices", _external=True))
|
||||
r = requests.post(url=url_for("api_invoices", _external=True), headers=header, data=json.dumps(data))
|
||||
r_json = r.json()
|
||||
#########Events##########
|
||||
|
||||
if "ERROR" in r_json:
|
||||
return jsonify({"status": "ERROR", "reason": r_json["ERROR"]}), 400
|
||||
|
||||
events_ext_db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO eventssold
|
||||
(uni, email, name, hash)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
unireg,
|
||||
email,
|
||||
name,
|
||||
r_json["payment_hash"]
|
||||
),
|
||||
@events_ext.route("/api/v1/events", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_events():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([event._asdict() for event in get_events(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/events", methods=["POST"])
|
||||
@events_ext.route("/api/v1/events/<event_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"wallet": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"info": {"type": "string", "min": 0, "required": True},
|
||||
"closing_date": {"type": "string", "empty": False, "required": True},
|
||||
"event_start_date": {"type": "string", "empty": False, "required": True},
|
||||
"event_end_date": {"type": "string", "empty": False, "required": True},
|
||||
"amount_tickets": {"type": "integer", "min": 0, "required": True},
|
||||
"price_per_ticket": {"type": "integer", "min": 0, "required": True}
|
||||
}
|
||||
)
|
||||
def api_event_create(event_id=None):
|
||||
if event_id:
|
||||
event = get_event(event_id)
|
||||
print(g.data)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if event.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
event = update_event(event_id, **g.data)
|
||||
else:
|
||||
event = create_event(**g.data)
|
||||
print(event)
|
||||
return jsonify(event._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_form_delete(event_id):
|
||||
event = get_event(event_id)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if event.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_event(event_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
|
||||
@events_ext.route("/api/v1/tickets", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_tickets():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in get_tickets(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["POST"])
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True}
|
||||
})
|
||||
def api_ticket_make_ticket(event_id, sats):
|
||||
|
||||
event = get_event(event_id)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"status": "TRUE", "pay_req": r_json["pay_req"], "payment_hash": r_json["payment_hash"]}), 200
|
||||
ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, event=event_id, **g.data)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/checkticket/", methods=["GET"])
|
||||
def api_checkticket():
|
||||
"""."""
|
||||
thehash = request.args.get("thehash")
|
||||
#Check databases
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
|
||||
if not eventssold:
|
||||
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
|
||||
if eventssold[0][4] == 0:
|
||||
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 200
|
||||
@events_ext.route("/api/v1/tickets/<checking_id>", methods=["GET"])
|
||||
def api_ticket_send_ticket(checking_id):
|
||||
theticket = get_ticket(checking_id)
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
events_ext_db.execute("UPDATE eventssold SET reg = 1 WHERE hash = ?", (thehash,))
|
||||
if is_paid:
|
||||
wallet = get_wallet(theticket.wallet)
|
||||
payment = wallet.get_payment(checking_id)
|
||||
payment.set_pending(False)
|
||||
ticket = update_ticket(paid=True, checking_id=checking_id)
|
||||
|
||||
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_ticket_delete(ticket_id):
|
||||
ticket = get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if ticket.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_ticket(ticket_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########EventTickets##########
|
||||
|
||||
@events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"])
|
||||
def api_event_tickets(wallet_id, event_id):
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in get_event_tickets(wallet_id=wallet_id, event_id=event_id)]), HTTPStatus.OK
|
||||
|
||||
@events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"])
|
||||
def api_event_register_ticket(ticket_id):
|
||||
|
||||
ticket = get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
if ticket.registered == True:
|
||||
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
|
||||
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in reg_ticket(ticket_id)]), HTTPStatus.OK
|
||||
|
||||
return jsonify({"status": "TRUE", "name": eventssold[0][3]}), 200
|
||||
|
||||
@@ -1,54 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5>
|
||||
<q-list>
|
||||
<q-item v-for="tool in tools" :key="tool.name" tag="a" :href="tool.url" target="_blank">
|
||||
{% raw %} <!-- with raw Flask won't try to interpret the Vue moustaches -->
|
||||
<q-item-section>
|
||||
<q-item-label>{{ tool.name }}</q-item-label>
|
||||
<q-item-label caption>{{ tool.language }}</q-item-label>
|
||||
</q-item-section>
|
||||
{% endraw %}
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
<p>A magical "g" is always available, with info about the user, wallets and extensions:</p>
|
||||
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tools: []
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
var self = this;
|
||||
|
||||
// axios is available for making requests
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: '/example/api/v1/tools',
|
||||
headers: {
|
||||
'X-example-header': 'not-used'
|
||||
}
|
||||
}).then(function (response) {
|
||||
self.tools = response.data;
|
||||
});
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="tool in tools"
|
||||
:key="tool.name"
|
||||
tag="a"
|
||||
:href="tool.url"
|
||||
target="_blank"
|
||||
>
|
||||
{% raw %}
|
||||
<!-- with raw Flask won't try to interpret the Vue moustaches -->
|
||||
<q-item-section>
|
||||
<q-item-label>{{ tool.name }}</q-item-label>
|
||||
<q-item-label caption>{{ tool.language }}</q-item-label>
|
||||
</q-item-section>
|
||||
{% endraw %}
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
<p>
|
||||
A magical "g" is always available, with info about the user, wallets and
|
||||
extensions:
|
||||
</p>
|
||||
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tools: []
|
||||
}
|
||||
});
|
||||
</script>
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
|
||||
// axios is available for making requests
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: '/example/api/v1/tools',
|
||||
headers: {
|
||||
'X-example-header': 'not-used'
|
||||
}
|
||||
}).then(function (response) {
|
||||
self.tools = response.data
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
# import requests
|
||||
|
||||
from flask import jsonify
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.extensions.example import example_ext
|
||||
|
||||
|
||||
@@ -34,4 +34,4 @@ def api_example():
|
||||
}
|
||||
]
|
||||
|
||||
return jsonify(tools), Status.OK
|
||||
return jsonify(tools), HTTPStatus.OK
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<h1>Support Tickets</h1>
|
||||
<h2>Get paid sats to answer questions</h2>
|
||||
Charge people per word for contacting you. Possible applications include, paid support ticketing, PAYG language services, contact spam protection.
|
||||
@@ -0,0 +1,8 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Support Tickets",
|
||||
"short_description": "LN support ticket system",
|
||||
"icon": "contact_support",
|
||||
"contributors": ["benarc"]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import Tickets, Forms
|
||||
|
||||
|
||||
|
||||
|
||||
#######TICKETS########
|
||||
|
||||
def create_ticket(checking_id: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets:
|
||||
with open_ext_db("lnticket") as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(checking_id, form, email, ltext, name, wallet, sats, False),
|
||||
)
|
||||
|
||||
return get_ticket(checking_id)
|
||||
|
||||
def update_ticket(paid: bool, checking_id: str) -> Tickets:
|
||||
with open_ext_db("lnticket") as db:
|
||||
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
|
||||
if row[7] == True:
|
||||
return get_ticket(checking_id)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE ticket
|
||||
SET paid = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(paid, checking_id),
|
||||
)
|
||||
|
||||
formdata = get_form(row[1])
|
||||
amount = formdata.amountmade + row[7]
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE forms
|
||||
SET amountmade = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(amount, row[1]),
|
||||
)
|
||||
return get_ticket(checking_id)
|
||||
|
||||
def get_ticket(ticket_id: str) -> Optional[Tickets]:
|
||||
with open_ext_db("lnticket") as db:
|
||||
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
|
||||
|
||||
return Tickets(**row) if row else None
|
||||
|
||||
|
||||
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("lnticket") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_ticket(ticket_id: str) -> None:
|
||||
with open_ext_db("lnticket") as db:
|
||||
db.execute("DELETE FROM ticket WHERE id = ?", (ticket_id,))
|
||||
|
||||
|
||||
|
||||
|
||||
########FORMS#########
|
||||
|
||||
|
||||
def create_form(*, wallet: str, name: str, description: str, costpword: int) -> Forms:
|
||||
with open_ext_db("lnticket") as db:
|
||||
form_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO forms (id, wallet, name, description, costpword, amountmade)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(form_id, wallet, name, description, costpword, 0 ),
|
||||
)
|
||||
|
||||
return get_form(form_id)
|
||||
|
||||
def update_form(form_id: str, **kwargs) -> Forms:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
with open_ext_db("lnticket") as db:
|
||||
db.execute(f"UPDATE forms SET {q} WHERE id = ?", (*kwargs.values(), form_id))
|
||||
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
|
||||
|
||||
return Forms(**row) if row else None
|
||||
|
||||
|
||||
def get_form(form_id: str) -> Optional[Forms]:
|
||||
with open_ext_db("lnticket") as db:
|
||||
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
|
||||
|
||||
return Forms(**row) if row else None
|
||||
|
||||
|
||||
def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("lnticket") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM forms WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Forms(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_form(form_id: str) -> None:
|
||||
with open_ext_db("lnticket") as db:
|
||||
db.execute("DELETE FROM forms WHERE id = ?", (form_id,))
|
||||
@@ -0,0 +1,94 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
def m001_initial(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS forms (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
costpword INTEGER NOT NULL,
|
||||
amountmade INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
form TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
ltext TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
sats INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def m002_changed(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS ticket (
|
||||
id TEXT PRIMARY KEY,
|
||||
form TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
ltext TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
sats INTEGER NOT NULL,
|
||||
paid BOOLEAN NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]:
|
||||
usescsv = ""
|
||||
|
||||
for i in range(row[5]):
|
||||
if row[7]:
|
||||
usescsv += "," + str(i + 1)
|
||||
else:
|
||||
usescsv += "," + str(1)
|
||||
usescsv = usescsv[1:]
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (
|
||||
id,
|
||||
form,
|
||||
email,
|
||||
ltext,
|
||||
name,
|
||||
wallet,
|
||||
sats,
|
||||
paid
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
row[0],
|
||||
row[1],
|
||||
row[2],
|
||||
row[3],
|
||||
row[4],
|
||||
row[5],
|
||||
row[6],
|
||||
True,
|
||||
),
|
||||
)
|
||||
db.execute("DROP TABLE tickets")
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("lnticket") as db:
|
||||
m001_initial(db)
|
||||
m002_changed(db)
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Forms(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
description: str
|
||||
costpword: int
|
||||
amountmade: int
|
||||
time: int
|
||||
|
||||
|
||||
class Tickets(NamedTuple):
|
||||
id: str
|
||||
form: str
|
||||
email: str
|
||||
ltext: str
|
||||
name: str
|
||||
wallet: str
|
||||
sats: int
|
||||
paid: bool
|
||||
time: int
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Support Tickets: Get paid sats to answer questions
|
||||
</h5>
|
||||
<p>
|
||||
Charge people per word for contacting you. Possible applications incude,
|
||||
paid support ticketing, PAYG language services, contact spam
|
||||
protection.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
@@ -0,0 +1,204 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h3 class="q-my-none">{{ form_name }}</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">{{ form_desc }}</h5>
|
||||
<br />
|
||||
<q-form @submit="Invoice()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Your name "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.email"
|
||||
type="email"
|
||||
label="Your email (optional, if you want a reply)"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.text"
|
||||
type="textarea"
|
||||
label="{{ form_costpword }} sats per word"
|
||||
></q-input>
|
||||
<p>{% raw %}{{amountWords}}{% endraw %}</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.name == '' || formDialog.data.text == ''"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
console.log('{{ form_costpword }}')
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
name: '',
|
||||
email: '',
|
||||
text: ''
|
||||
}
|
||||
},
|
||||
receive: {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
amountWords() {
|
||||
var regex = /\s+/gi
|
||||
var char = this.formDialog.data.text
|
||||
.trim()
|
||||
.replace(regex, ' ')
|
||||
.split(' ').length
|
||||
this.formDialog.data.sats = char * parseInt('{{ form_costpword }}')
|
||||
if (this.formDialog.data.sats == parseInt('{{ form_costpword }}')) {
|
||||
return '0 Sats to pay'
|
||||
} else {
|
||||
return this.formDialog.data.sats + ' Sats to pay'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetForm: function (e) {
|
||||
e.preventDefault()
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
this.formDialog.data.text = ''
|
||||
this.formDialog.data.sats = 0
|
||||
},
|
||||
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker
|
||||
dismissMsg()
|
||||
|
||||
clearInterval(paymentChecker)
|
||||
setTimeout(function () {}, 10000)
|
||||
},
|
||||
Invoice: function () {
|
||||
var self = this
|
||||
axios
|
||||
.post(
|
||||
'/lnticket/api/v1/tickets/' +
|
||||
'{{ form_id }}/' +
|
||||
self.formDialog.data.sats,
|
||||
{
|
||||
form: '{{ form_id }}',
|
||||
name: self.formDialog.data.name,
|
||||
email: self.formDialog.data.email,
|
||||
ltext: self.formDialog.data.text,
|
||||
sats: self.formDialog.data.sats
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.checking_id
|
||||
|
||||
dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
|
||||
self.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: self.paymentReq
|
||||
}
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios
|
||||
.get('/lnticket/api/v1/tickets/' + self.paymentCheck)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
dismissMsg()
|
||||
self.formDialog.data.name = ''
|
||||
self.formDialog.data.email = ''
|
||||
self.formDialog.data.text = ''
|
||||
self.formDialog.data.sats = 0
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: null
|
||||
})
|
||||
self.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,443 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
>New Form</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Forms</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportformsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="forms"
|
||||
row-key="id"
|
||||
:columns="formsTable.columns"
|
||||
:pagination.sync="formsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="link"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateformDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteForm(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportticketsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="email"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'mailto:' + props.row.email"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTicket(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
LNbits Support Tickets extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "lnticket/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Form name "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.description"
|
||||
type="textarea"
|
||||
label="Description "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.costpword"
|
||||
type="number"
|
||||
label="Amount per word"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update Form</q-btn
|
||||
>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.costpword == null || formDialog.data.costpword < 0 || formDialog.data.name == null"
|
||||
type="submit"
|
||||
>Create Form</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapLNTicket = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.displayUrl = ['/lnticket/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
forms: [],
|
||||
tickets: [],
|
||||
formsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Description',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'costpword',
|
||||
align: 'left',
|
||||
label: 'Cost Per Word',
|
||||
field: 'costpword'
|
||||
},
|
||||
{
|
||||
name: 'amountmade',
|
||||
align: 'left',
|
||||
label: 'Amount Made',
|
||||
field: 'amountmade'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
||||
{name: 'ltext', align: 'left', label: 'Ticket', field: 'ltext'},
|
||||
{name: 'sats', align: 'left', label: 'Paid', field: 'sats'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTickets: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/lnticket/api/v1/tickets?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
return mapLNTicket(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteTicket: function (ticketId) {
|
||||
var self = this
|
||||
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this ticket')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnticket/api/v1/tickets/' + ticketId,
|
||||
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = _.reject(self.tickets, function (obj) {
|
||||
return obj.id == ticketId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportticketsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||
},
|
||||
|
||||
getForms: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/lnticket/api/v1/forms?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.forms = response.data.map(function (obj) {
|
||||
return mapLNTicket(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
sendFormData: function () {
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
var data = this.formDialog.data
|
||||
|
||||
if (data.id) {
|
||||
this.updateForm(wallet, data)
|
||||
} else {
|
||||
this.createForm(wallet, data)
|
||||
}
|
||||
},
|
||||
|
||||
createForm: function (wallet, data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
|
||||
.then(function (response) {
|
||||
self.forms.push(mapLNTicket(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.forms, {id: formId})
|
||||
console.log(link.id)
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
this.formDialog.data.name = link.name
|
||||
this.formDialog.data.description = link.description
|
||||
this.formDialog.data.costpword = link.costpword
|
||||
this.formDialog.show = true
|
||||
},
|
||||
updateForm: function (wallet, data) {
|
||||
var self = this
|
||||
console.log(data)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/lnticket/api/v1/forms/' + data.id,
|
||||
wallet.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.forms = _.reject(self.forms, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.forms.push(mapLNTicket(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteForm: function (formsId) {
|
||||
var self = this
|
||||
var forms = _.findWhere(this.forms, {id: formsId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this form link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnticket/api/v1/forms/' + formsId,
|
||||
_.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.forms = _.reject(self.forms, function (obj) {
|
||||
return obj.id == formsId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportformsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.formsTable.columns, this.forms)
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTickets()
|
||||
this.getForms()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
from flask import g, abort, render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.extensions.lnticket import lnticket_ext
|
||||
from .crud import get_form
|
||||
|
||||
|
||||
@lnticket_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
return render_template("lnticket/index.html", user=g.user)
|
||||
|
||||
|
||||
@lnticket_ext.route("/<form_id>")
|
||||
def display(form_id):
|
||||
form = get_form(form_id) or abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.")
|
||||
print(form.id)
|
||||
|
||||
return render_template("lnticket/display.html", form_id=form.id, form_name=form.name, form_desc=form.description, form_costpword=form.costpword)
|
||||
@@ -0,0 +1,143 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.extensions.lnticket import lnticket_ext
|
||||
from .crud import create_ticket, update_ticket, get_ticket, get_tickets, delete_ticket, create_form, update_form, get_form, get_forms, delete_form
|
||||
|
||||
|
||||
#########FORMS##########
|
||||
|
||||
@lnticket_ext.route("/api/v1/forms", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_forms():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([form._asdict() for form in get_forms(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/forms", methods=["POST"])
|
||||
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"wallet": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"description": {"type": "string", "min": 0, "required": True},
|
||||
"costpword": {"type": "integer", "min": 0, "required": True}
|
||||
}
|
||||
)
|
||||
def api_form_create(form_id=None):
|
||||
if form_id:
|
||||
form = get_form(form_id)
|
||||
print(g.data)
|
||||
|
||||
if not form:
|
||||
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if form.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
form = update_form(form_id, **g.data)
|
||||
else:
|
||||
form = create_form(**g.data)
|
||||
return jsonify(form._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_form_delete(form_id):
|
||||
form = get_form(form_id)
|
||||
|
||||
if not form:
|
||||
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if form.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_form(form_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
#########tickets##########
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_tickets():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets/<form_id>/<sats>", methods=["POST"])
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"form": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True},
|
||||
"ltext": {"type": "string", "empty": False, "required": True},
|
||||
"sats": {"type": "integer", "min": 0, "required": True}
|
||||
})
|
||||
def api_ticket_make_ticket(form_id, sats):
|
||||
|
||||
event = get_form(form_id)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {form_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, **g.data)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets/<checking_id>", methods=["GET"])
|
||||
def api_ticket_send_ticket(checking_id):
|
||||
theticket = get_ticket(checking_id)
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if is_paid:
|
||||
wallet = get_wallet(theticket.wallet)
|
||||
payment = wallet.get_payment(checking_id)
|
||||
payment.set_pending(False)
|
||||
ticket = update_ticket(paid=True, checking_id=checking_id)
|
||||
|
||||
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_ticket_delete(ticket_id):
|
||||
ticket = get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if ticket.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_ticket(ticket_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Paywall",
|
||||
"short_description": "Create paywalls for content",
|
||||
"icon": "vpn_lock",
|
||||
"icon": "policy",
|
||||
"contributors": ["eillarra"]
|
||||
}
|
||||
|
||||
@@ -6,15 +6,17 @@ from lnbits.helpers import urlsafe_short_hash
|
||||
from .models import Paywall
|
||||
|
||||
|
||||
def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall:
|
||||
def create_paywall(
|
||||
*, wallet_id: str, url: str, memo: str, description: Optional[str] = None, amount: int = 0, remembers: bool = True
|
||||
) -> Paywall:
|
||||
with open_ext_db("paywall") as db:
|
||||
paywall_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO paywalls (id, wallet, secret, url, memo, amount)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO paywalls (id, wallet, url, memo, description, amount, remembers)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(paywall_id, wallet_id, urlsafe_short_hash(), url, memo, amount),
|
||||
(paywall_id, wallet_id, url, memo, description, amount, int(remembers)),
|
||||
)
|
||||
|
||||
return get_paywall(paywall_id)
|
||||
@@ -24,7 +26,7 @@ def get_paywall(paywall_id: str) -> Optional[Paywall]:
|
||||
with open_ext_db("paywall") as db:
|
||||
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
|
||||
|
||||
return Paywall(**row) if row else None
|
||||
return Paywall.from_row(row) if row else None
|
||||
|
||||
|
||||
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
|
||||
@@ -35,7 +37,7 @@ def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Paywall(**row) for row in rows]
|
||||
return [Paywall.from_row(row) for row in rows]
|
||||
|
||||
|
||||
def delete_paywall(paywall_id: str) -> None:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from sqlite3 import OperationalError
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
@@ -20,6 +22,52 @@ def m001_initial(db):
|
||||
)
|
||||
|
||||
|
||||
def m002_redux(db):
|
||||
"""
|
||||
Creates an improved paywalls table and migrates the existing data.
|
||||
"""
|
||||
try:
|
||||
db.execute("SELECT remembers FROM paywalls")
|
||||
|
||||
except OperationalError:
|
||||
db.execute("ALTER TABLE paywalls RENAME TO paywalls_old")
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS paywalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
memo TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
amount INTEGER DEFAULT 0,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
remembers INTEGER DEFAULT 0,
|
||||
extras TEXT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON paywalls (wallet)")
|
||||
|
||||
for row in [list(row) for row in db.fetchall("SELECT * FROM paywalls_old")]:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO paywalls (
|
||||
id,
|
||||
wallet,
|
||||
url,
|
||||
memo,
|
||||
amount,
|
||||
time
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(row[0], row[1], row[3], row[4], row[5], row[6]),
|
||||
)
|
||||
|
||||
db.execute("DROP TABLE paywalls_old")
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("paywall") as db:
|
||||
m001_initial(db)
|
||||
m002_redux(db)
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
from typing import NamedTuple
|
||||
import json
|
||||
|
||||
from sqlite3 import Row
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class Paywall(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
secret: str
|
||||
url: str
|
||||
memo: str
|
||||
description: str
|
||||
amount: int
|
||||
time: int
|
||||
remembers: bool
|
||||
extras: Optional[dict]
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Paywall":
|
||||
data = dict(row)
|
||||
data["remembers"] = bool(data["remembers"])
|
||||
data["extras"] = json.loads(data["extras"]) if data["extras"] else None
|
||||
return cls(**data)
|
||||
|
||||
@@ -7,21 +7,140 @@
|
||||
<q-expansion-item group="api" dense expand-separator label="List paywalls">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
<code><span class="text-blue">GET</span> /paywall/api/v1/paywalls</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<paywall_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}paywall/api/v1/paywalls -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create a paywall">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
<code
|
||||
><span class="text-green">POST</span> /paywall/api/v1/paywalls</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"amount": <integer>, "description": <string>, "memo":
|
||||
<string>, "remembers": <boolean>, "url":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"amount": <integer>, "description": <string>, "id":
|
||||
<string>, "memo": <string>, "remembers": <boolean>,
|
||||
"time": <int>, "url": <string>, "wallet":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}paywall/api/v1/paywalls -d
|
||||
'{"url": <string>, "memo": <string>, "description":
|
||||
<string>, "amount": <integer>, "remembers":
|
||||
<boolean>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Delete a paywall" class="q-pb-md">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Create an invoice (public)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
<code
|
||||
><span class="text-green">POST</span>
|
||||
/paywall/api/v1/paywalls/<paywall_id>/invoice</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"amount": <integer>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"checking_id": <string>, "payment_request":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root
|
||||
}}paywall/api/v1/paywalls/<paywall_id>/invoice -d '{"amount":
|
||||
<integer>}' -H "Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Check invoice status (public)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-green">POST</span>
|
||||
/paywall/api/v1/paywalls/<paywall_id>/check_invoice</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"checking_id": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>{"paid": false}</code><br />
|
||||
<code
|
||||
>{"paid": true, "url": <string>, "remembers":
|
||||
<boolean>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root
|
||||
}}paywall/api/v1/paywalls/<paywall_id>/check_invoice -d
|
||||
'{"checking_id": <string>}' -H "Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a paywall"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/paywall/api/v1/paywalls/<paywall_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}paywall/api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -1,107 +1,163 @@
|
||||
{% extends "public.html" %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h5 class="text-subtitle1 q-my-none">{{ paywall.memo }}</h5>
|
||||
<strong class="text-purple">Price: <lnbits-fsat :amount="{{ paywall.amount }}"></lnbits-fsat> sat</strong>
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
<div v-if="paymentReq">
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-sm-8 col-md-5 col-lg-4">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h5 class="text-subtitle1 q-mt-none q-mb-sm">{{ paywall.memo }}</h5>
|
||||
{% if paywall.description %}
|
||||
<p>{{ paywall.description }}</p>
|
||||
{% endif %}
|
||||
<div v-if="!this.redirectUrl" class="q-mt-lg">
|
||||
<q-form v-if="">
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="userAmount"
|
||||
type="number"
|
||||
:min="paywallAmount"
|
||||
suffix="sat"
|
||||
label="Choose an amount *"
|
||||
:hint="'Minimum ' + paywallAmount + ' sat'"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="check"
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
@click="createInvoice"
|
||||
:disabled="userAmount < paywallAmount || paymentReq"
|
||||
></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
<div v-if="paymentReq" class="q-mt-lg">
|
||||
<a :href="'lightning:' + paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode :value="paymentReq" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(paymentReq)">Copy invoice</q-btn>
|
||||
<q-btn outline color="grey" @click="copyText(paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
<q-btn @click="cancelPayment" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="redirectUrl">
|
||||
<p>You can access the URL behind this paywall:<br>
|
||||
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong></p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" type="a" :href="redirectUrl">Open URL</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
<p>
|
||||
You can access the URL behind this paywall:<br />
|
||||
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong>
|
||||
</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" type="a" :href="redirectUrl"
|
||||
>Open URL</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits paywall</h6>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode);
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paymentReq: null,
|
||||
redirectUrl: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getInvoice: function () {
|
||||
var self = this;
|
||||
|
||||
axios.get(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice'
|
||||
).then(function (response) {
|
||||
self.paymentReq = response.data.payment_request;
|
||||
|
||||
dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
});
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios.post(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
|
||||
{checking_id: response.data.checking_id}
|
||||
).then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker);
|
||||
dismissMsg();
|
||||
self.redirectUrl = res.data.url;
|
||||
self.$q.localStorage.set('lnbits.paywall.{{ paywall.id }}', res.data.url);
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Payment received!',
|
||||
icon: null
|
||||
});
|
||||
}
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
}, 2000);
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
userAmount: {{ paywall.amount }},
|
||||
paywallAmount: {{ paywall.amount }},
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
paymentDialog: {
|
||||
dismissMsg: null,
|
||||
checker: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
amount: function () {
|
||||
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelPayment: function () {
|
||||
this.paymentReq = null
|
||||
clearInterval(this.paymentDialog.checker)
|
||||
if (this.paymentDialog.dismissMsg) {
|
||||
this.paymentDialog.dismissMsg()
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}');
|
||||
createInvoice: function () {
|
||||
var self = this
|
||||
|
||||
if (url) {
|
||||
this.redirectUrl = url;
|
||||
} else {
|
||||
this.getInvoice();
|
||||
};
|
||||
axios
|
||||
.post(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice',
|
||||
{amount: this.amount}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request.toUpperCase()
|
||||
|
||||
self.paymentDialog.dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
|
||||
self.paymentDialog.checker = setInterval(function () {
|
||||
axios
|
||||
.post(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
|
||||
{checking_id: response.data.checking_id}
|
||||
)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
self.cancelPayment()
|
||||
self.redirectUrl = res.data.url
|
||||
if (res.data.remembers) {
|
||||
self.$q.localStorage.set(
|
||||
'lnbits.paywall.{{ paywall.id }}',
|
||||
res.data.url
|
||||
)
|
||||
}
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Payment received!',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
},
|
||||
created: function () {
|
||||
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}')
|
||||
|
||||
if (url) {
|
||||
this.redirectUrl = url
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,214 +1,312 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
>New paywall</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true">New paywall</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Paywalls</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Paywalls</h5>
|
||||
</div>
|
||||
<q-table dense flat
|
||||
:data="paywalls"
|
||||
row-key="id"
|
||||
:columns="paywallsTable.columns"
|
||||
:pagination.sync="paywallsTable.pagination">
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.displayUrl" target="_blank"></q-btn>
|
||||
<q-btn unelevated dense size="xs" icon="link" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.url" target="_blank"></q-btn>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn flat dense size="xs" @click="deletePaywall(props.row.id)" icon="cancel" color="pink"></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits paywall extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "paywall/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="createPaywall" class="q-gutter-md">
|
||||
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
|
||||
</q-select>
|
||||
<q-input filled dense
|
||||
v-model.trim="formDialog.data.url"
|
||||
type="url"
|
||||
label="Target URL *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.number="formDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount (sat) *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.trim="formDialog.data.memo"
|
||||
label="Memo"
|
||||
placeholder="LNbits invoice"></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null"
|
||||
type="submit">Create paywall</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="paywalls"
|
||||
row-key="id"
|
||||
:columns="paywallsTable.columns"
|
||||
:pagination.sync="paywallsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="launch"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="link"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.url"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deletePaywall(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
<script>
|
||||
var mapPaywall = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
|
||||
obj.displayUrl = ['/paywall/', obj.id].join('');
|
||||
return obj;
|
||||
}
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits paywall extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "paywall/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paywalls: [],
|
||||
paywallsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
|
||||
{
|
||||
name: 'amount', align: 'right', label: 'Amount (sat)', field: 'fsat', sortable: true,
|
||||
sort: function (a, b, rowA, rowB) {
|
||||
return rowA.amount - rowB.amount;
|
||||
}
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="createPaywall" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.url"
|
||||
type="url"
|
||||
label="Redirect URL *"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.memo"
|
||||
label="Title *"
|
||||
placeholder="LNbits paywall"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
autogrow
|
||||
v-model.trim="formDialog.data.description"
|
||||
label="Description"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount (sat) *"
|
||||
hint="This is the minimum amount users can pay/donate."
|
||||
></q-input>
|
||||
<q-list>
|
||||
<q-item tag="label" class="rounded-borders">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
v-model="formDialog.data.remembers"
|
||||
color="deep-purple"
|
||||
></q-checkbox>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Remember payments</q-item-label>
|
||||
<q-item-label caption
|
||||
>A succesful payment will be registered in the browser's
|
||||
storage, so the user doesn't need to pay again to access the
|
||||
URL.</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null || formDialog.data.memo == null"
|
||||
type="submit"
|
||||
>Create paywall</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapPaywall = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.displayUrl = ['/paywall/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paywalls: [],
|
||||
paywallsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'right',
|
||||
label: 'Amount (sat)',
|
||||
field: 'fsat',
|
||||
sortable: true,
|
||||
sort: function (a, b, rowA, rowB) {
|
||||
return rowA.amount - rowB.amount
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
},
|
||||
{
|
||||
name: 'remembers',
|
||||
align: 'left',
|
||||
label: 'Remember',
|
||||
field: 'remembers'
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: 'Date',
|
||||
field: 'date',
|
||||
sortable: true
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getPaywalls: function () {
|
||||
var self = this;
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
remembers: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPaywalls: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api.request(
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/paywall/api/v1/paywalls?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
).then(function (response) {
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paywalls = response.data.map(function (obj) {
|
||||
return mapPaywall(obj);
|
||||
});
|
||||
});
|
||||
},
|
||||
createPaywall: function () {
|
||||
var data = {
|
||||
url: this.formDialog.data.url,
|
||||
memo: this.formDialog.data.memo,
|
||||
amount: this.formDialog.data.amount
|
||||
};
|
||||
var self = this;
|
||||
return mapPaywall(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
createPaywall: function () {
|
||||
var data = {
|
||||
url: this.formDialog.data.url,
|
||||
memo: this.formDialog.data.memo,
|
||||
amount: this.formDialog.data.amount,
|
||||
description: this.formDialog.data.description,
|
||||
remembers: this.formDialog.data.remembers
|
||||
}
|
||||
var self = this
|
||||
|
||||
LNbits.api.request(
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/paywall/api/v1/paywalls',
|
||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey,
|
||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
|
||||
.inkey,
|
||||
data
|
||||
).then(function (response) {
|
||||
self.paywalls.push(mapPaywall(response.data));
|
||||
self.formDialog.show = false;
|
||||
self.formDialog.data = {};
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
},
|
||||
deletePaywall: function (paywallId) {
|
||||
var self = this;
|
||||
var paywall = _.findWhere(this.paywalls, {id: paywallId});
|
||||
|
||||
LNbits.utils.confirmDialog(
|
||||
'Are you sure you want to delete this paywall link?'
|
||||
).onOk(function () {
|
||||
LNbits.api.request(
|
||||
'DELETE',
|
||||
'/paywall/api/v1/paywalls/' + paywallId,
|
||||
_.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey
|
||||
).then(function (response) {
|
||||
self.paywalls = _.reject(self.paywalls, function (obj) { return obj.id == paywallId; });
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls);
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paywalls.push(mapPaywall(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {
|
||||
remembers: false
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getPaywalls();
|
||||
}
|
||||
deletePaywall: function (paywallId) {
|
||||
var self = this
|
||||
var paywall = _.findWhere(this.paywalls, {id: paywallId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this paywall link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/paywall/api/v1/paywalls/' + paywallId,
|
||||
_.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paywalls = _.reject(self.paywalls, function (obj) {
|
||||
return obj.id == paywallId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getPaywalls()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from flask import g, abort, render_template
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from lnbits.extensions.paywall import paywall_ext
|
||||
from .crud import get_paywall
|
||||
@@ -16,6 +16,6 @@ def index():
|
||||
|
||||
@paywall_ext.route("/<paywall_id>")
|
||||
def display(paywall_id):
|
||||
paywall = get_paywall(paywall_id) or abort(Status.NOT_FOUND, "Paywall does not exist.")
|
||||
paywall = get_paywall(paywall_id) or abort(HTTPStatus.NOT_FOUND, "Paywall does not exist.")
|
||||
|
||||
return render_template("paywall/display.html", paywall=paywall)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.extensions.paywall import paywall_ext
|
||||
@@ -18,7 +18,7 @@ def api_paywalls():
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), Status.OK
|
||||
return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls", methods=["POST"])
|
||||
@@ -27,13 +27,15 @@ def api_paywalls():
|
||||
schema={
|
||||
"url": {"type": "string", "empty": False, "required": True},
|
||||
"memo": {"type": "string", "empty": False, "required": True},
|
||||
"description": {"type": "string", "empty": True, "nullable": True, "required": False},
|
||||
"amount": {"type": "integer", "min": 0, "required": True},
|
||||
"remembers": {"type": "boolean", "required": True},
|
||||
}
|
||||
)
|
||||
def api_paywall_create():
|
||||
paywall = create_paywall(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify(paywall._asdict()), Status.CREATED
|
||||
return jsonify(paywall._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"])
|
||||
@@ -42,28 +44,33 @@ def api_paywall_delete(paywall_id):
|
||||
paywall = get_paywall(paywall_id)
|
||||
|
||||
if not paywall:
|
||||
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if paywall.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your paywall."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your paywall."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_paywall(paywall_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["GET"])
|
||||
def api_paywall_get_invoice(paywall_id):
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"])
|
||||
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
|
||||
def api_paywall_create_invoice(paywall_id):
|
||||
paywall = get_paywall(paywall_id)
|
||||
|
||||
if g.data["amount"] < paywall.amount:
|
||||
return jsonify({"message": f"Minimum amount is {paywall.amount} sat."}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
try:
|
||||
amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=paywall.wallet, amount=paywall.amount, memo=f"#paywall {paywall.memo}"
|
||||
wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.OK
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
|
||||
@@ -72,18 +79,18 @@ def api_paywal_check_invoice(paywall_id):
|
||||
paywall = get_paywall(paywall_id)
|
||||
|
||||
if not paywall:
|
||||
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(g.data["checking_id"]).pending
|
||||
except Exception:
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
if is_paid:
|
||||
wallet = get_wallet(paywall.wallet)
|
||||
payment = wallet.get_payment(g.data["checking_id"])
|
||||
payment.set_pending(False)
|
||||
|
||||
return jsonify({"paid": True, "url": paywall.url}), Status.OK
|
||||
return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
@@ -24,7 +24,7 @@ def get_tpos(tpos_id: str) -> Optional[TPoS]:
|
||||
with open_ext_db("tpos") as db:
|
||||
row = db.fetchone("SELECT * FROM tposs WHERE id = ?", (tpos_id,))
|
||||
|
||||
return TPoS(**row) if row else None
|
||||
return TPoS.from_row(row) if row else None
|
||||
|
||||
|
||||
def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
|
||||
@@ -35,7 +35,7 @@ def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM tposs WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [TPoS(**row) for row in rows]
|
||||
return [TPoS.from_row(row) for row in rows]
|
||||
|
||||
|
||||
def delete_tpos(tpos_id: str) -> None:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from sqlite3 import Row
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
@@ -6,3 +7,7 @@ class TPoS(NamedTuple):
|
||||
wallet: str
|
||||
name: str
|
||||
currency: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "TPoS":
|
||||
return cls(**dict(row))
|
||||
|
||||
@@ -4,49 +4,74 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="List all users TPoS">
|
||||
<q-expansion-item group="api" dense expand-separator label="List TPoS">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-blue">GET</span> /tpos/api/v1/tposs</code>
|
||||
<code><span class="text-blue">GET</span> /tpos/api/v1/tposs</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
|
||||
<code>{"currency": <string>, "id": <string>, "name": <string>, "wallet": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<tpos_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X GET {{ request.url_root }}tpos/api/v1/tposs -H "X-Api-Key: <invoice_key>" </code>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}tpos/api/v1/tposs -H "X-Api-Key:
|
||||
<invoice_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create a TPoS">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-green">POST</span> /tpos/api/v1/tposs</code>
|
||||
<code><span class="text-green">POST</span> /tpos/api/v1/tposs</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"name": <string>, "currency": <string*ie USD*>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
|
||||
<code>{"currency": <string>, "id": <string>, "name": <string>, "wallet": <string>}</code>
|
||||
<code
|
||||
>{"name": <string>, "currency": <string*ie USD*>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"currency": <string>, "id": <string>, "name":
|
||||
<string>, "wallet": <string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X POST {{ request.url_root }}tpos/api/v1/tposs -d '{"name": <string>, "currency": <string>}' -H "Content-type: application/json" -H "X-Api-Key: <admin_key>"
|
||||
</code>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}tpos/api/v1/tposs -d '{"name":
|
||||
<string>, "currency": <string>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: <admin_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item group="api" dense expand-separator label="Delete a TPoS" class="q-pb-md">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a TPoS"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-green">DELETE</span> /tpos/api/v1/tposs/<tpos_id></code>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/tpos/api/v1/tposs/<tpos_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 NO_CONTENT</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X DELETE {{ request.url_root }}tpos/api/v1/tposs/<tpos_id> -H "X-Api-Key: <admin_key>"
|
||||
</code>
|
||||
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}tpos/api/v1/tposs/<tpos_id> -H "X-Api-Key: <admin_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="info"
|
||||
label="About TPoS">
|
||||
<q-expansion-item group="extras" icon="info" label="About TPoS">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>Thiago's Point of Sale is a secure, mobile-ready, instant and shareable point of sale terminal (PoS) for merchants. The PoS is linked to your LNbits wallet but completely air-gapped so users can ONLY create invoices. To share the TPoS hit the hash on the terminal.</p>
|
||||
<small>Created by <a href="https://github.com/talvasconcelos" target="_blank">Tiago Vasconcelos</a>.</small>
|
||||
<p>
|
||||
Thiago's Point of Sale is a secure, mobile-ready, instant and shareable
|
||||
point of sale terminal (PoS) for merchants. The PoS is linked to your
|
||||
LNbits wallet but completely air-gapped so users can ONLY create
|
||||
invoices. To share the TPoS hit the hash on the terminal.
|
||||
</p>
|
||||
<small
|
||||
>Created by
|
||||
<a href="https://github.com/talvasconcelos" target="_blank"
|
||||
>Tiago Vasconcelos</a
|
||||
>.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -1,221 +1,423 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
>New TPoS</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true">New TPoS</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">TPoS</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">TPoS</h5>
|
||||
</div>
|
||||
<q-table dense flat
|
||||
:data="tposs"
|
||||
row-key="id"
|
||||
:columns="tpossTable.columns"
|
||||
:pagination.sync="tpossTable.pagination">
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tposs"
|
||||
row-key="id"
|
||||
:columns="tpossTable.columns"
|
||||
:pagination.sync="tpossTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.tpos" target="_blank"></q-btn>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn flat dense size="xs" @click="deleteTPoS(props.row.id)" icon="cancel" color="pink"></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits TPoS extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "tpos/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
{% include "tpos/_tpos.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="createTPoS" class="q-gutter-md">
|
||||
<q-input filled dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
label="Name"
|
||||
placeholder="Tiago's PoS"></q-input>
|
||||
<q-select filled dense
|
||||
emit-value v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"></q-select>
|
||||
<q-select filled dense
|
||||
emit-value v-model="formDialog.data.currency"
|
||||
:options="currencyOptions"
|
||||
label="Currency *"></q-select>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.currency == null || formDialog.data.name == null"
|
||||
type="submit">Create TPoS</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="launch"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.tpos"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTPoS(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
<script>
|
||||
var mapTPoS = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
|
||||
obj.tpos = ['/tpos/', obj.id].join('');
|
||||
return obj;
|
||||
}
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits TPoS extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "tpos/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
{% include "tpos/_tpos.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tposs: [],
|
||||
currencyOptions: [
|
||||
'USD','EUR','GBP','AED','AFN','ALL','AMD','ANG','AOA','ARS','AUD','AWG','AZN','BAM','BBD','BDT','BGN','BHD',
|
||||
'BIF','BMD','BND','BOB','BRL','BSD','BTN','BWP','BYN','BZD','CAD','CDF','CHF','CLF','CLP','CNH','CNY','COP',
|
||||
'CRC','CUC','CUP','CVE','CZK','DJF','DKK','DOP','DZD','EGP','ERN','ETB','EUR','FJD','FKP','GBP','GEL','GGP',
|
||||
'GHS','GIP','GMD','GNF','GTQ','GYD','HKD','HNL','HRK','HTG','HUF','IDR','ILS','IMP','INR','IQD','IRR','ISK',
|
||||
'JEP','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KWD','KYD','KZT','LAK','LBP','LKR','LRD','LSL',
|
||||
'LYD','MAD','MDL','MGA','MKD','MMK','MNT','MOP','MRO','MUR','MVR','MWK','MXN','MYR','MZN','NAD','NGN','NIO','NOK','NPR','NZD','OMR','PAB','PEN','PGK','PHP','PKR','PLN','PYG','QAR','RON','RSD','RUB','RWF','SAR','SBD',
|
||||
'SCR','SDG','SEK','SGD','SHP','SLL','SOS','SRD','SSP','STD','SVC','SYP','SZL','THB','TJS','TMT','TND','TOP',
|
||||
'TRY','TTD','TWD','TZS','UAH','UGX','USD','UYU','UZS','VEF','VES','VND','VUV','WST','XAF','XAG','XAU','XCD',
|
||||
'XDR','XOF','XPD','XPF','XPT','YER','ZAR','ZMW','ZWL'
|
||||
],
|
||||
tpossTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'currency', align: 'left', label: 'Currency', field: 'currency'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
|
||||
<q-form @submit="createTPoS" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
label="Name"
|
||||
placeholder="Tiago's PoS"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
></q-select>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.currency"
|
||||
:options="currencyOptions"
|
||||
label="Currency *"
|
||||
></q-select>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.currency == null || formDialog.data.name == null"
|
||||
type="submit"
|
||||
>Create TPoS</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapTPoS = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.tpos = ['/tpos/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tposs: [],
|
||||
currencyOptions: [
|
||||
'USD',
|
||||
'EUR',
|
||||
'GBP',
|
||||
'AED',
|
||||
'AFN',
|
||||
'ALL',
|
||||
'AMD',
|
||||
'ANG',
|
||||
'AOA',
|
||||
'ARS',
|
||||
'AUD',
|
||||
'AWG',
|
||||
'AZN',
|
||||
'BAM',
|
||||
'BBD',
|
||||
'BDT',
|
||||
'BGN',
|
||||
'BHD',
|
||||
'BIF',
|
||||
'BMD',
|
||||
'BND',
|
||||
'BOB',
|
||||
'BRL',
|
||||
'BSD',
|
||||
'BTN',
|
||||
'BWP',
|
||||
'BYN',
|
||||
'BZD',
|
||||
'CAD',
|
||||
'CDF',
|
||||
'CHF',
|
||||
'CLF',
|
||||
'CLP',
|
||||
'CNH',
|
||||
'CNY',
|
||||
'COP',
|
||||
'CRC',
|
||||
'CUC',
|
||||
'CUP',
|
||||
'CVE',
|
||||
'CZK',
|
||||
'DJF',
|
||||
'DKK',
|
||||
'DOP',
|
||||
'DZD',
|
||||
'EGP',
|
||||
'ERN',
|
||||
'ETB',
|
||||
'EUR',
|
||||
'FJD',
|
||||
'FKP',
|
||||
'GBP',
|
||||
'GEL',
|
||||
'GGP',
|
||||
'GHS',
|
||||
'GIP',
|
||||
'GMD',
|
||||
'GNF',
|
||||
'GTQ',
|
||||
'GYD',
|
||||
'HKD',
|
||||
'HNL',
|
||||
'HRK',
|
||||
'HTG',
|
||||
'HUF',
|
||||
'IDR',
|
||||
'ILS',
|
||||
'IMP',
|
||||
'INR',
|
||||
'IQD',
|
||||
'IRR',
|
||||
'ISK',
|
||||
'JEP',
|
||||
'JMD',
|
||||
'JOD',
|
||||
'JPY',
|
||||
'KES',
|
||||
'KGS',
|
||||
'KHR',
|
||||
'KMF',
|
||||
'KPW',
|
||||
'KRW',
|
||||
'KWD',
|
||||
'KYD',
|
||||
'KZT',
|
||||
'LAK',
|
||||
'LBP',
|
||||
'LKR',
|
||||
'LRD',
|
||||
'LSL',
|
||||
'LYD',
|
||||
'MAD',
|
||||
'MDL',
|
||||
'MGA',
|
||||
'MKD',
|
||||
'MMK',
|
||||
'MNT',
|
||||
'MOP',
|
||||
'MRO',
|
||||
'MUR',
|
||||
'MVR',
|
||||
'MWK',
|
||||
'MXN',
|
||||
'MYR',
|
||||
'MZN',
|
||||
'NAD',
|
||||
'NGN',
|
||||
'NIO',
|
||||
'NOK',
|
||||
'NPR',
|
||||
'NZD',
|
||||
'OMR',
|
||||
'PAB',
|
||||
'PEN',
|
||||
'PGK',
|
||||
'PHP',
|
||||
'PKR',
|
||||
'PLN',
|
||||
'PYG',
|
||||
'QAR',
|
||||
'RON',
|
||||
'RSD',
|
||||
'RUB',
|
||||
'RWF',
|
||||
'SAR',
|
||||
'SBD',
|
||||
'SCR',
|
||||
'SDG',
|
||||
'SEK',
|
||||
'SGD',
|
||||
'SHP',
|
||||
'SLL',
|
||||
'SOS',
|
||||
'SRD',
|
||||
'SSP',
|
||||
'STD',
|
||||
'SVC',
|
||||
'SYP',
|
||||
'SZL',
|
||||
'THB',
|
||||
'TJS',
|
||||
'TMT',
|
||||
'TND',
|
||||
'TOP',
|
||||
'TRY',
|
||||
'TTD',
|
||||
'TWD',
|
||||
'TZS',
|
||||
'UAH',
|
||||
'UGX',
|
||||
'USD',
|
||||
'UYU',
|
||||
'UZS',
|
||||
'VEF',
|
||||
'VES',
|
||||
'VND',
|
||||
'VUV',
|
||||
'WST',
|
||||
'XAF',
|
||||
'XAG',
|
||||
'XAU',
|
||||
'XCD',
|
||||
'XDR',
|
||||
'XOF',
|
||||
'XPD',
|
||||
'XPF',
|
||||
'XPT',
|
||||
'YER',
|
||||
'ZAR',
|
||||
'ZMW',
|
||||
'ZWL'
|
||||
],
|
||||
tpossTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{
|
||||
name: 'currency',
|
||||
align: 'left',
|
||||
label: 'Currency',
|
||||
field: 'currency'
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {};
|
||||
},
|
||||
getTPoSs: function () {
|
||||
var self = this;
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {}
|
||||
},
|
||||
getTPoSs: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api.request(
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/tpos/api/v1/tposs?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
).then(function (response) {
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tposs = response.data.map(function (obj) {
|
||||
return mapTPoS(obj);
|
||||
});
|
||||
});
|
||||
},
|
||||
createTPoS: function () {
|
||||
var data = {
|
||||
name: this.formDialog.data.name,
|
||||
currency: this.formDialog.data.currency
|
||||
};
|
||||
var self = this;
|
||||
return mapTPoS(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
createTPoS: function () {
|
||||
var data = {
|
||||
name: this.formDialog.data.name,
|
||||
currency: this.formDialog.data.currency
|
||||
}
|
||||
var self = this
|
||||
|
||||
LNbits.api.request(
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/tpos/api/v1/tposs',
|
||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey,
|
||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
|
||||
.inkey,
|
||||
data
|
||||
).then(function (response) {
|
||||
self.tposs.push(mapTPoS(response.data));
|
||||
self.formDialog.show = false;
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
},
|
||||
deleteTPoS: function (tposId) {
|
||||
var self = this;
|
||||
var tpos = _.findWhere(this.tposs, {id: tposId});
|
||||
|
||||
LNbits.utils.confirmDialog(
|
||||
'Are you sure you want to delete this TPoS?'
|
||||
).onOk(function () {
|
||||
LNbits.api.request(
|
||||
'DELETE',
|
||||
'/tpos/api/v1/tposs/' + tposId,
|
||||
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
|
||||
).then(function (response) {
|
||||
self.tposs = _.reject(self.tposs, function (obj) { return obj.id == tposId; });
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs);
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tposs.push(mapTPoS(response.data))
|
||||
self.formDialog.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTPoSs();
|
||||
}
|
||||
deleteTPoS: function (tposId) {
|
||||
var self = this
|
||||
var tpos = _.findWhere(this.tposs, {id: tposId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this TPoS?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/tpos/api/v1/tposs/' + tposId,
|
||||
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tposs = _.reject(self.tposs, function (obj) {
|
||||
return obj.id == tposId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTPoSs()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,225 +1,265 @@
|
||||
{% extends "public.html" %}
|
||||
|
||||
|
||||
{% block toolbar_title %}{{ tpos.name }}{% endblock %}
|
||||
|
||||
{% block footer %}{% endblock %}
|
||||
|
||||
{% block page_container %}
|
||||
<q-page-container>
|
||||
<q-page>
|
||||
<q-page-sticky v-if="exchangeRate" expand position="top">
|
||||
<div class="row justify-center full-width">
|
||||
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
|
||||
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
||||
<h5 class="q-mt-none">
|
||||
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
|
||||
</h5>
|
||||
{% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% endblock
|
||||
%} {% block footer %}{% endblock %} {% block page_container %}
|
||||
<q-page-container>
|
||||
<q-page>
|
||||
<q-page-sticky v-if="exchangeRate" expand position="top">
|
||||
<div class="row justify-center full-width">
|
||||
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
|
||||
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
||||
<h5 class="q-mt-none">
|
||||
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</q-page-sticky>
|
||||
<q-page-sticky expand position="bottom">
|
||||
<div class="row justify-center full-width">
|
||||
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
|
||||
<div class="keypad q-pa-sm">
|
||||
<q-btn unelevated @click="stack.push(1)" size="xl" color="grey-8"
|
||||
>1</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(2)" size="xl" color="grey-8"
|
||||
>2</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(3)" size="xl" color="grey-8"
|
||||
>3</q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="stack = []"
|
||||
size="xl"
|
||||
color="pink"
|
||||
class="btn-cancel"
|
||||
>C</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(4)" size="xl" color="grey-8"
|
||||
>4</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(5)" size="xl" color="grey-8"
|
||||
>5</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(6)" size="xl" color="grey-8"
|
||||
>6</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(7)" size="xl" color="grey-8"
|
||||
>7</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(8)" size="xl" color="grey-8"
|
||||
>8</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(9)" size="xl" color="grey-8"
|
||||
>9</q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
:disabled="amount == 0"
|
||||
@click="showInvoice()"
|
||||
size="xl"
|
||||
color="green"
|
||||
class="btn-confirm"
|
||||
>OK</q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="stack.splice(-1, 1)"
|
||||
size="xl"
|
||||
color="grey-7"
|
||||
>DEL</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(0)" size="xl" color="grey-8"
|
||||
>0</q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="urlDialog.show = true"
|
||||
size="xl"
|
||||
color="grey-7"
|
||||
>#</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</q-page-sticky>
|
||||
<q-page-sticky expand position="bottom">
|
||||
<div class="row justify-center full-width">
|
||||
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
|
||||
<div class="keypad q-pa-sm">
|
||||
<q-btn unelevated
|
||||
@click="stack.push(1)"
|
||||
size="xl" color="grey-8">1</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(2)"
|
||||
size="xl" color="grey-8">2</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(3)"
|
||||
size="xl" color="grey-8">3</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack = []"
|
||||
size="xl" color="pink" class="btn-cancel">C</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(4)"
|
||||
size="xl" color="grey-8">4</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(5)"
|
||||
size="xl" color="grey-8">5</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(6)"
|
||||
size="xl" color="grey-8">6</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(7)"
|
||||
size="xl" color="grey-8">7</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(8)"
|
||||
size="xl" color="grey-8">8</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(9)"
|
||||
size="xl" color="grey-8">9</q-btn>
|
||||
<q-btn unelevated
|
||||
:disabled="amount == 0"
|
||||
@click="showInvoice()"
|
||||
size="xl" color="green" class="btn-confirm">OK</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.splice(-1, 1)"
|
||||
size="xl" color="grey-7">DEL</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="stack.push(0)"
|
||||
size="xl" color="grey-8">0</q-btn>
|
||||
<q-btn unelevated
|
||||
@click="urlDialog.show = true"
|
||||
size="xl" color="grey-7">#</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-page-sticky>
|
||||
<q-dialog
|
||||
v-model="invoiceDialog.show"
|
||||
position="top"
|
||||
@hide="closeInvoiceDialog"
|
||||
>
|
||||
<q-card
|
||||
v-if="invoiceDialog.data"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="invoiceDialog.data.payment_request"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<div class="text-center">
|
||||
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
||||
<h5 class="q-mt-none">
|
||||
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
|
||||
</h5>
|
||||
</div>
|
||||
</q-page-sticky>
|
||||
<q-dialog v-model="invoiceDialog.show" position="top" @hide="closeInvoiceDialog">
|
||||
<q-card v-if="invoiceDialog.data" class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode :value="invoiceDialog.data.payment_request" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
</q-responsive>
|
||||
<div class="text-center">
|
||||
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
||||
<h5 class="q-mt-none">
|
||||
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="urlDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode value="{{ request.url }}" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
</q-responsive>
|
||||
<div class="text-center q-mb-xl">
|
||||
<p style="word-break: break-all"><strong>{{ tpos.name }}</strong><br>{{ request.url }}</p>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')">Copy URL</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %}
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="urlDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
value="{{ request.url }}"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<div class="text-center q-mb-xl">
|
||||
<p style="word-break: break-all;">
|
||||
<strong>{{ tpos.name }}</strong><br />{{ request.url }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')"
|
||||
>Copy URL</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %} {% block styles %}
|
||||
<style>
|
||||
.keypad {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
}
|
||||
.keypad .btn {
|
||||
height: 100%;
|
||||
}
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
grid-row: auto/span 2;
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.keypad {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
}
|
||||
.keypad .btn {
|
||||
height: 100%;
|
||||
}
|
||||
.btn-cancel, .btn-confirm {
|
||||
grid-row: auto/span 2;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode);
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tposId: '{{ tpos.id }}',
|
||||
currency: '{{ tpos.currency }}',
|
||||
exchangeRate: null,
|
||||
stack: [],
|
||||
invoiceDialog: {
|
||||
show: false,
|
||||
data: null,
|
||||
dismissMsg: null,
|
||||
paymentChecker: null
|
||||
},
|
||||
urlDialog: {
|
||||
show: false
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
amount: function () {
|
||||
if (!this.stack.length) return 0.00;
|
||||
return (Number(this.stack.join('')) / 100).toFixed(2);
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tposId: '{{ tpos.id }}',
|
||||
currency: '{{ tpos.currency }}',
|
||||
exchangeRate: null,
|
||||
stack: [],
|
||||
invoiceDialog: {
|
||||
show: false,
|
||||
data: null,
|
||||
dismissMsg: null,
|
||||
paymentChecker: null
|
||||
},
|
||||
famount: function () {
|
||||
return LNbits.utils.formatCurrency(this.amount, this.currency);
|
||||
},
|
||||
sat: function () {
|
||||
if (!this.exchangeRate) return 0;
|
||||
return Math.ceil((this.amount / this.exchangeRate) * 100000000);
|
||||
},
|
||||
fsat: function () {
|
||||
return LNbits.utils.formatSat(this.sat);
|
||||
urlDialog: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
amount: function () {
|
||||
if (!this.stack.length) return 0.0
|
||||
return (Number(this.stack.join('')) / 100).toFixed(2)
|
||||
},
|
||||
methods: {
|
||||
closeInvoiceDialog: function () {
|
||||
this.stack = [];
|
||||
var dialog = this.invoiceDialog;
|
||||
setTimeout(function () {
|
||||
clearInterval(dialog.paymentChecker);
|
||||
dialog.dismissMsg();
|
||||
}, 3000);
|
||||
},
|
||||
showInvoice: function () {
|
||||
var self = this;
|
||||
var dialog = this.invoiceDialog;
|
||||
famount: function () {
|
||||
return LNbits.utils.formatCurrency(this.amount, this.currency)
|
||||
},
|
||||
sat: function () {
|
||||
if (!this.exchangeRate) return 0
|
||||
return Math.ceil((this.amount / this.exchangeRate) * 100000000)
|
||||
},
|
||||
fsat: function () {
|
||||
return LNbits.utils.formatSat(this.sat)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeInvoiceDialog: function () {
|
||||
this.stack = []
|
||||
var dialog = this.invoiceDialog
|
||||
setTimeout(function () {
|
||||
clearInterval(dialog.paymentChecker)
|
||||
dialog.dismissMsg()
|
||||
}, 3000)
|
||||
},
|
||||
showInvoice: function () {
|
||||
var self = this
|
||||
var dialog = this.invoiceDialog
|
||||
|
||||
axios.post(
|
||||
'/tpos/api/v1/tposs/' + this.tposId + '/invoices/',
|
||||
{amount: this.sat}
|
||||
).then(function (response) {
|
||||
dialog.data = response.data;
|
||||
dialog.show = true;
|
||||
axios
|
||||
.post('/tpos/api/v1/tposs/' + this.tposId + '/invoices/', {
|
||||
amount: this.sat
|
||||
})
|
||||
.then(function (response) {
|
||||
dialog.data = response.data
|
||||
dialog.show = true
|
||||
|
||||
dialog.dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
});
|
||||
})
|
||||
|
||||
dialog.paymentChecker = setInterval(function () {
|
||||
axios.get(
|
||||
'/tpos/api/v1/tposs/' + self.tposId + '/invoices/' + response.data.checking_id
|
||||
).then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(dialog.paymentChecker);
|
||||
dialog.dismissMsg();
|
||||
dialog.show = false;
|
||||
axios
|
||||
.get(
|
||||
'/tpos/api/v1/tposs/' +
|
||||
self.tposId +
|
||||
'/invoices/' +
|
||||
response.data.checking_id
|
||||
)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(dialog.paymentChecker)
|
||||
dialog.dismissMsg()
|
||||
dialog.show = false
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: self.fsat + ' sat received!',
|
||||
icon: null
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
},
|
||||
getRates: function () {
|
||||
var self = this;
|
||||
axios.get("https://api.opennode.co/v1/rates").then(function (response) {
|
||||
self.exchangeRate = response.data.data['BTC' + self.currency][self.currency];
|
||||
});
|
||||
}
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: self.fsat + ' sat received!',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
created: function () {
|
||||
var getRates = this.getRates;
|
||||
getRates();
|
||||
setInterval(function () { getRates(); }, 20000);
|
||||
getRates: function () {
|
||||
var self = this
|
||||
axios.get('https://api.opennode.co/v1/rates').then(function (response) {
|
||||
self.exchangeRate =
|
||||
response.data.data['BTC' + self.currency][self.currency]
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
},
|
||||
created: function () {
|
||||
var getRates = this.getRates
|
||||
getRates()
|
||||
setInterval(function () {
|
||||
getRates()
|
||||
}, 20000)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from flask import g, abort, render_template
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from lnbits.extensions.tpos import tpos_ext
|
||||
from .crud import get_tpos
|
||||
@@ -16,6 +16,6 @@ def index():
|
||||
|
||||
@tpos_ext.route("/<tpos_id>")
|
||||
def tpos(tpos_id):
|
||||
tpos = get_tpos(tpos_id) or abort(Status.NOT_FOUND, "TPoS does not exist.")
|
||||
tpos = get_tpos(tpos_id) or abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.")
|
||||
|
||||
return render_template("tpos/tpos.html", tpos=tpos)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.extensions.tpos import tpos_ext
|
||||
@@ -18,7 +18,7 @@ def api_tposs():
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([tpos._asdict() for tpos in get_tposs(wallet_ids)]), Status.OK
|
||||
return jsonify([tpos._asdict() for tpos in get_tposs(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@tpos_ext.route("/api/v1/tposs", methods=["POST"])
|
||||
@@ -32,7 +32,7 @@ def api_tposs():
|
||||
def api_tpos_create():
|
||||
tpos = create_tpos(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify(tpos._asdict()), Status.CREATED
|
||||
return jsonify(tpos._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@tpos_ext.route("/api/v1/tposs/<tpos_id>", methods=["DELETE"])
|
||||
@@ -41,14 +41,14 @@ def api_tpos_delete(tpos_id):
|
||||
tpos = get_tpos(tpos_id)
|
||||
|
||||
if not tpos:
|
||||
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if tpos.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your TPoS."}), Status.FORBIDDEN
|
||||
return jsonify({"message": "Not your TPoS."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_tpos(tpos_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/", methods=["POST"])
|
||||
@@ -57,16 +57,16 @@ def api_tpos_create_invoice(tpos_id):
|
||||
tpos = get_tpos(tpos_id)
|
||||
|
||||
if not tpos:
|
||||
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.CREATED
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<checking_id>", methods=["GET"])
|
||||
@@ -74,18 +74,18 @@ def api_tpos_check_invoice(tpos_id, checking_id):
|
||||
tpos = get_tpos(tpos_id)
|
||||
|
||||
if not tpos:
|
||||
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
|
||||
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
if is_paid:
|
||||
wallet = get_wallet(tpos.wallet)
|
||||
payment = wallet.get_payment(checking_id)
|
||||
payment.set_pending(False)
|
||||
|
||||
return jsonify({"paid": True}), Status.OK
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<h1>User Manager</h1>
|
||||
<h2>Make and manager users/wallets</h2>
|
||||
To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the develpoers stack as the user and wallet manager.
|
||||
@@ -0,0 +1,8 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
usermanager_ext: Blueprint = Blueprint("usermanager", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "User Manager",
|
||||
"short_description": "Generate users and wallets",
|
||||
"icon": "person_add",
|
||||
"contributors": ["benarc"]
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.settings import WALLET
|
||||
from .models import Users, Wallets
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from ...core.crud import (
|
||||
create_account,
|
||||
get_user,
|
||||
update_user_extension,
|
||||
get_wallet_payments,
|
||||
create_wallet,
|
||||
delete_wallet,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###Users
|
||||
|
||||
def create_usermanager_user(user_name: str, wallet_name: str, admin_id: str) -> Users:
|
||||
user = get_user(create_account().id)
|
||||
|
||||
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
|
||||
with open_ext_db("usermanager") as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO users (id, name, admin)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(user.id, user_name, admin_id),
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO wallets (id, admin, name, user, adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey)
|
||||
)
|
||||
|
||||
return get_usermanager_user(user.id)
|
||||
|
||||
|
||||
def get_usermanager_user(user_id: str) -> Users:
|
||||
with open_ext_db("usermanager") as db:
|
||||
|
||||
row = db.fetchone("SELECT * FROM users WHERE id = ?", (user_id,))
|
||||
|
||||
|
||||
return Users(**row) if row else None
|
||||
|
||||
|
||||
def get_usermanager_users(user_id: str) -> Users:
|
||||
|
||||
with open_ext_db("usermanager") as db:
|
||||
rows = db.fetchall("SELECT * FROM users WHERE admin = ?", (user_id,))
|
||||
|
||||
return [Users(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_usermanager_user(user_id: str) -> None:
|
||||
row = get_usermanager_wallets(user_id)
|
||||
print("test")
|
||||
with open_ext_db("usermanager") as db:
|
||||
db.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||
row
|
||||
for r in row:
|
||||
delete_wallet( user_id=user_id, wallet_id=r.id)
|
||||
with open_ext_db("usermanager") as dbb:
|
||||
dbb.execute("DELETE FROM wallets WHERE user = ?", (user_id,))
|
||||
|
||||
###Wallets
|
||||
|
||||
def create_usermanager_wallet(user_id: str, wallet_name: str, admin_id: str) -> Wallets:
|
||||
wallet = create_wallet(user_id=user_id, wallet_name=wallet_name)
|
||||
with open_ext_db("usermanager") as db:
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO wallets (id, admin, name, user, adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey)
|
||||
)
|
||||
|
||||
return get_usermanager_wallet(wallet.id)
|
||||
|
||||
def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]:
|
||||
with open_ext_db("usermanager") as db:
|
||||
row = db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,))
|
||||
|
||||
return Wallets(**row) if row else None
|
||||
|
||||
|
||||
def get_usermanager_wallets(user_id: str) -> Wallets:
|
||||
|
||||
with open_ext_db("usermanager") as db:
|
||||
rows = db.fetchall("SELECT * FROM wallets WHERE admin = ?", (user_id,))
|
||||
|
||||
return [Wallets(**row) for row in rows]
|
||||
|
||||
|
||||
def get_usermanager_wallet_transactions(wallet_id: str) -> Users:
|
||||
return get_wallet_payments(wallet_id=wallet_id,include_all_pending=False)
|
||||
|
||||
def get_usermanager_wallet_balances(user_id: str) -> Users:
|
||||
user = get_user(user_id)
|
||||
return (user.wallets)
|
||||
|
||||
|
||||
def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None:
|
||||
delete_wallet( user_id=user_id, wallet_id=wallet_id)
|
||||
with open_ext_db("usermanager") as db:
|
||||
db.execute("DELETE FROM wallets WHERE id = ?", (wallet_id,))
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
"""
|
||||
Initial users table.
|
||||
"""
|
||||
db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
admin TEXT NOT NULL,
|
||||
email TEXT,
|
||||
password TEXT
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
"""
|
||||
Initial wallets table.
|
||||
"""
|
||||
db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id TEXT PRIMARY KEY,
|
||||
admin TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
adminkey TEXT NOT NULL,
|
||||
inkey TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("usermanager") as db:
|
||||
m001_initial(db)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
class Users(NamedTuple):
|
||||
id: str
|
||||
name: str
|
||||
admin: str
|
||||
email: str
|
||||
password: str
|
||||
|
||||
class Wallets(NamedTuple):
|
||||
id: str
|
||||
admin: str
|
||||
name: str
|
||||
user: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
User Manager: Make and manager users/wallets
|
||||
</h5>
|
||||
<p>
|
||||
To help developers use LNbits to manage their users, the User Manager
|
||||
extension allows the creation and management of users and wallets.
|
||||
<br />For example, a games developer may be developing a game that needs
|
||||
each user to have their own wallet, LNbits can be included in the
|
||||
develpoers stack as the user and wallet manager.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET users">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/usermanager/api/v1/users</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>JSON list of users</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}usermanager/api/v1/users -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET wallets">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/usermanager/api/v1/wallets/<user_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>JSON wallet data</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}usermanager/api/v1/wallets/<user_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET transactions">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/usermanager/api/v1/wallets<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>JSON a wallets transactions</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}usermanager/api/v1/wallets<wallet_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="POST user + initial wallet"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-green">POST</span>
|
||||
/usermanager/api/v1/users</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": <string>, "Content-type":
|
||||
"application/json"}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json) - "admin_id" is a YOUR user ID
|
||||
</h5>
|
||||
<code
|
||||
>{"admin_id": <string>, "user_name": <string>,
|
||||
"wallet_name": <string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"checking_id": <string>,"payment_request":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/users -d
|
||||
'{"admin_id": "{{ g.user.id }}", "wallet_name": <string>,
|
||||
"user_name": <string>}' -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="POST wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-green">POST</span>
|
||||
/usermanager/api/v1/wallets</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": <string>, "Content-type":
|
||||
"application/json"}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json) - "admin_id" is a YOUR user ID
|
||||
</h5>
|
||||
<code
|
||||
>{"user_id": <string>, "wallet_name": <string>,
|
||||
"admin_id": <string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"checking_id": <string>,"payment_request":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d
|
||||
'{"user_id": <string>, "wallet_name": <string>,
|
||||
"admin_id": "{{ g.user.id }}"}' -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="DELETE user and their wallets"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-red">DELETE</span>
|
||||
/usermanager/api/v1/users/<user_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="DELETE wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-red">DELETE</span>
|
||||
/usermanager/api/v1/wallets/<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="POST activate extension"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-green">POST</span>
|
||||
/usermanager/api/v1/extensions</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d
|
||||
'{"userid": <string>, "extension": <string>, "active":
|
||||
<integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
||||
"Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
@@ -0,0 +1,453 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="userDialog.show = true"
|
||||
>New User</q-btn
|
||||
>
|
||||
<q-btn unelevated color="deep-purple" @click="walletDialog.show = true"
|
||||
>New Wallet
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Users</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportUsersCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="users"
|
||||
row-key="id"
|
||||
:columns="usersTable.columns"
|
||||
:pagination.sync="usersTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteUser(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Wallets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportWalletsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="wallets"
|
||||
row-key="id"
|
||||
:columns="walletsTable.columns"
|
||||
:pagination.sync="walletsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="account_balance_wallet"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.walllink"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-tooltip>
|
||||
Link to wallet
|
||||
</q-tooltip>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteWallet(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits User Manager Extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "usermanager/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="userDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
|
||||
<q-form @submit="sendUserFormData" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="userDialog.data.usrname"
|
||||
label="Username"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="userDialog.data.walname"
|
||||
label="Initial wallet name"
|
||||
></q-input>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="userDialog.data.walname == null"
|
||||
type="submit"
|
||||
>Create User</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="walletDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
|
||||
<q-form @submit="sendWalletFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="walletDialog.data.user"
|
||||
:options="userOptions"
|
||||
label="User *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="walletDialog.data.walname"
|
||||
label="Wallet name"
|
||||
></q-input>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="walletDialog.data.walname == null"
|
||||
type="submit"
|
||||
>Create Wallet</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapUserManager = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.walllink = ['../wallet?usr=', obj.user, '&wal=', obj.id].join('')
|
||||
obj._data = _.clone(obj)
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
wallets: [],
|
||||
users: [],
|
||||
|
||||
usersTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Username', field: 'name'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
walletsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'user', align: 'left', label: 'User', field: 'user'},
|
||||
{
|
||||
name: 'adminkey',
|
||||
align: 'left',
|
||||
label: 'Admin Key',
|
||||
field: 'adminkey'
|
||||
},
|
||||
{name: 'inkey', align: 'left', label: 'Invoice Key', field: 'inkey'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
walletDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
userDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userOptions: function () {
|
||||
return this.users.map(function (obj) {
|
||||
console.log(obj.id)
|
||||
return {
|
||||
value: String(obj.id),
|
||||
label: String(obj.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
///////////////Users////////////////////////////
|
||||
|
||||
getUsers: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/usermanager/api/v1/users',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users = response.data.map(function (obj) {
|
||||
return mapUserManager(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
openUserUpdateDialog: function (linkId) {
|
||||
var link = _.findWhere(this.users, {id: linkId})
|
||||
|
||||
this.userDialog.data = _.clone(link._data)
|
||||
this.userDialog.show = true
|
||||
},
|
||||
sendUserFormData: function () {
|
||||
if (this.userDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
admin_id: this.g.user.id,
|
||||
user_name: this.userDialog.data.usrname,
|
||||
wallet_name: this.userDialog.data.walname
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
this.createUser(data)
|
||||
}
|
||||
},
|
||||
|
||||
createUser: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/usermanager/api/v1/users',
|
||||
this.g.user.wallets[0].inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users.push(mapUserManager(response.data))
|
||||
self.userDialog.show = false
|
||||
self.userDialog.data = {}
|
||||
data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteUser: function (userId) {
|
||||
var self = this
|
||||
|
||||
console.log(userId)
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this User link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/usermanager/api/v1/users/' + userId,
|
||||
self.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users = _.reject(self.users, function (obj) {
|
||||
return obj.id == userId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
exportUsersCSV: function () {
|
||||
LNbits.utils.exportCSV(this.usersTable.columns, this.users)
|
||||
},
|
||||
|
||||
///////////////Wallets////////////////////////////
|
||||
|
||||
getWallets: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/usermanager/api/v1/wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets = response.data.map(function (obj) {
|
||||
return mapUserManager(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
openWalletUpdateDialog: function (linkId) {
|
||||
var link = _.findWhere(this.users, {id: linkId})
|
||||
|
||||
this.walletDialog.data = _.clone(link._data)
|
||||
this.walletDialog.show = true
|
||||
},
|
||||
sendWalletFormData: function () {
|
||||
if (this.walletDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
user_id: this.walletDialog.data.user,
|
||||
admin_id: this.g.user.id,
|
||||
wallet_name: this.walletDialog.data.walname
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
this.createWallet(data)
|
||||
}
|
||||
},
|
||||
|
||||
createWallet: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/usermanager/api/v1/wallets',
|
||||
this.g.user.wallets[0].inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets.push(mapUserManager(response.data))
|
||||
self.walletDialog.show = false
|
||||
self.walletDialog.data = {}
|
||||
data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteWallet: function (userId) {
|
||||
var self = this
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/usermanager/api/v1/wallets/' + userId,
|
||||
self.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets = _.reject(self.wallets, function (obj) {
|
||||
return obj.id == userId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportWalletsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getUsers()
|
||||
this.getWallets()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
from flask import g, abort, render_template, jsonify
|
||||
import json
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.usermanager import usermanager_ext
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
@usermanager_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
|
||||
return render_template("usermanager/index.html", user=g.user)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user