Merge branch 'main' into diagon-alley

This commit is contained in:
Tiago vasconcelos 2022-08-18 16:50:58 +01:00
commit 627914abda
17 changed files with 134 additions and 52 deletions

View file

@ -9,9 +9,9 @@ jobs:
postgres: postgres:
image: postgres:latest image: postgres:latest
env: env:
POSTGRES_USER: postgres POSTGRES_USER: lnbits
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: lnbits
POSTGRES_DB: postgres POSTGRES_DB: migration
ports: ports:
# maps tcp port 5432 on service container to the host # maps tcp port 5432 on service container to the host
- 5432:5432 - 5432:5432
@ -36,11 +36,4 @@ jobs:
sudo apt install unzip sudo apt install unzip
- name: Run migrations - name: Run migrations
run: | run: |
rm -rf ./data make test-migration
mkdir -p ./data
export LNBITS_DATA_FOLDER="./data"
unzip tests/data/mock_data.zip -d ./data
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
poetry run python tools/conv.py

View file

@ -32,11 +32,13 @@ test:
FAKE_WALLET_SECRET="ToTheMoon1" \ FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest poetry run pytest
test-real-wallet: test-real-wallet:
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest poetry run pytest
test-venv: test-venv:
@ -44,7 +46,27 @@ test-venv:
FAKE_WALLET_SECRET="ToTheMoon1" \ FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests ./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-migration:
rm -rf ./migration-data
mkdir -p ./migration-data
unzip tests/data/mock_data.zip -d ./migration-data
HOST=0.0.0.0 \
PORT=5002 \
LNBITS_DATA_FOLDER="./migration-data" \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
HOST=0.0.0.0 \
PORT=5002 \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
LNBITS_DATA_FOLDER="./migration-data" \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
poetry run python tools/conv.py
migration:
poetry run python tools/conv.py
bak: bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres # LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres

View file

@ -48,4 +48,25 @@ LNbits currently supports SQLite and PostgreSQL databases. There is a migration
### Adding mock data to `mock_data.zip` ### Adding mock data to `mock_data.zip`
`mock_data.zip` contains a few lines of sample SQLite data and is used in automated GitHub test to see whether your migration in `conv.py` works. Run your extension and save a few lines of data into a SQLite `your_extension.sqlite3` file. Unzip `tests/data/mock_data.zip`, add `your_extension.sqlite3` and zip it again. Add the updated `mock_data.zip` to your PR. `mock_data.zip` contains a few lines of sample SQLite data and is used in automated GitHub test to see whether your migration in `conv.py` works. Run your extension and save a few lines of data into a SQLite `your_extension.sqlite3` file. Unzip `tests/data/mock_data.zip`, add `your_extension.sqlite3`, updated `database.sqlite3` and zip it again. Add the updated `mock_data.zip` to your PR.
### running migration locally
you will need a running postgres database
#### create lnbits user for migration database
```console
sudo su - postgres -c "psql -c 'CREATE ROLE lnbits LOGIN PASSWORD 'lnbits';'"
```
#### create migration database
```console
sudo su - postgres -c "psql -c 'CREATE DATABASE migration;'"
```
#### run the migration
```console
make test-migration
```
sudo su - postgres -c "psql -c 'CREATE ROLE lnbits LOGIN PASSWORD 'lnbits';'"
#### clean migration database afterwards, fails if you try again
```console
sudo su - postgres -c "psql -c 'DROP DATABASE IF EXISTS migration;'"
```

View file

@ -170,8 +170,9 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# START LNbits # START LNbits
# STOP LNbits # STOP LNbits
# on the LNBits folder, locate and edit 'tools/conv.py' with the relevant credentials poetry run python tools/conv.py
python3 tools/conv.py # or
make migration
``` ```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly. Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
@ -194,15 +195,14 @@ Description=LNbits
[Service] [Service]
# replace with the absolute path of your lnbits installation # replace with the absolute path of your lnbits installation
WorkingDirectory=/home/bitcoin/lnbits WorkingDirectory=/home/lnbits/lnbits-legend
# same here # same here. run `which poetry` if you can't find the poetry binary
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 ExecStart=/home/lnbits/.local/bin/poetry run lnbits
# replace with the user that you're running lnbits on # replace with the user that you're running lnbits on
User=bitcoin User=lnbits
Restart=always Restart=always
TimeoutSec=120 TimeoutSec=120
RestartSec=30 RestartSec=30
# this makes sure that you receive logs in real time
Environment=PYTHONUNBUFFERED=1 Environment=PYTHONUNBUFFERED=1
[Install] [Install]

View file

@ -668,7 +668,17 @@ new Vue({
}) })
}, },
exportCSV: function () { exportCSV: function () {
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments) // status is important for export but it is not in paymentsTable
// because it is manually added with payment detail link and icons
// and would cause duplication in the list
let columns = this.paymentsTable.columns
columns.unshift({
name: 'pending',
align: 'left',
label: 'Pending',
field: 'pending'
})
LNbits.utils.exportCSV(columns, this.payments)
} }
}, },
watch: { watch: {

View file

@ -1,7 +1,7 @@
import asyncio import asyncio
import binascii
import hashlib import hashlib
import json import json
from binascii import unhexlify
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO from io import BytesIO
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
@ -152,11 +152,23 @@ class CreateInvoiceData(BaseModel):
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
if data.description_hash: if data.description_hash:
description_hash = unhexlify(data.description_hash) try:
description_hash = binascii.unhexlify(data.description_hash)
except binascii.Error:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' must be a valid hex string",
)
unhashed_description = b"" unhashed_description = b""
memo = "" memo = ""
elif data.unhashed_description: elif data.unhashed_description:
unhashed_description = unhexlify(data.unhashed_description) try:
unhashed_description = binascii.unhexlify(data.unhashed_description)
except binascii.Error:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'unhashed_description' must be a valid hex string",
)
description_hash = b"" description_hash = b""
memo = "" memo = ""
else: else:

View file

@ -130,10 +130,13 @@ async def get_key_type(
# 2: invalid # 2: invalid
pathname = r["path"].split("/")[1] pathname = r["path"].split("/")[1]
if not api_key_header and not api_key_query: token = api_key_header or api_key_query
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
token = api_key_header if api_key_header else api_key_query if not token:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
try: try:
admin_checker = WalletAdminKeyChecker(api_key=token) admin_checker = WalletAdminKeyChecker(api_key=token)
@ -180,7 +183,14 @@ async def require_admin_key(
api_key_header: str = Security(api_key_header), # type: ignore api_key_header: str = Security(api_key_header), # type: ignore
api_key_query: str = Security(api_key_query), # type: ignore api_key_query: str = Security(api_key_query), # type: ignore
): ):
token = api_key_header if api_key_header else api_key_query
token = api_key_header or api_key_query
if not token:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Admin key required.",
)
wallet = await get_key_type(r, token) wallet = await get_key_type(r, token)
@ -199,11 +209,12 @@ async def require_invoice_key(
api_key_header: str = Security(api_key_header), # type: ignore api_key_header: str = Security(api_key_header), # type: ignore
api_key_query: str = Security(api_key_query), # type: ignore api_key_query: str = Security(api_key_query), # type: ignore
): ):
token = api_key_header or api_key_query token = api_key_header or api_key_query
if token is None: if not token:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.", detail="Invoice (or Admin) key required.",
) )

View file

@ -13,7 +13,7 @@
Charge people for using your domain name...<br /> Charge people for using your domain name...<br />
<a <a
href="https://github.com/lnbits/lnbits/tree/master/lnbits/extensions/lnaddress" href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/lnaddress"
>More details</a >More details</a
> >
<br /> <br />

View file

@ -296,16 +296,17 @@
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="link"
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
>Shareable link</q-btn ><q-tooltip>Copy sharable link</q-tooltip>
> </q-btn>
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="nfc" icon="nfc"
@click="writeNfcTag(qrCodeDialog.data.lnurl)" @click="writeNfcTag(qrCodeDialog.data.lnurl)"
:disable="nfcTagWriting" :disable="nfcTagWriting"
> ><q-tooltip>Write to NFC</q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
outline outline
@ -314,7 +315,8 @@
type="a" type="a"
:href="qrCodeDialog.data.print_url" :href="qrCodeDialog.data.print_url"
target="_blank" target="_blank"
></q-btn> ><q-tooltip>Print</q-tooltip></q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>

View file

@ -232,7 +232,7 @@
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="share" icon="link"
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
><q-tooltip>Copy shareable link</q-tooltip></q-btn ><q-tooltip>Copy shareable link</q-tooltip></q-btn
> >

View file

@ -13,7 +13,7 @@
Charge people for using your subdomain name...<br /> Charge people for using your subdomain name...<br />
<a <a
href="https://github.com/lnbits/lnbits/tree/master/lnbits/extensions/subdomains" href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/subdomains"
>More details</a >More details</a
> >
<br /> <br />

View file

@ -76,10 +76,10 @@ async def get_tipjars(wallet_id: str) -> Optional[list]:
async def delete_tipjar(tipjar_id: int) -> None: async def delete_tipjar(tipjar_id: int) -> None:
"""Delete a TipJar and all corresponding Tips""" """Delete a TipJar and all corresponding Tips"""
await db.execute("DELETE FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE tipjar = ?", (tipjar_id,)) rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE tipjar = ?", (tipjar_id,))
for row in rows: for row in rows:
await delete_tip(row["id"]) await delete_tip(row["id"])
await db.execute("DELETE FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
async def get_tip(tip_id: str) -> Optional[Tip]: async def get_tip(tip_id: str) -> Optional[Tip]:

View file

@ -418,16 +418,18 @@
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="link"
@click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')"
>Shareable link</q-btn ><q-tooltip>Copy sharable link</q-tooltip>
> </q-btn>
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="nfc" icon="nfc"
@click="writeNfcTag(qrCodeDialog.data.lnurl)" @click="writeNfcTag(qrCodeDialog.data.lnurl)"
:disable="nfcTagWriting" :disable="nfcTagWriting"
></q-btn> ><q-tooltip>Write to NFC</q-tooltip></q-btn
>
<q-btn <q-btn
outline outline
color="grey" color="grey"
@ -435,7 +437,8 @@
type="a" type="a"
:href="qrCodeDialog.data.print_url" :href="qrCodeDialog.data.print_url"
target="_blank" target="_blank"
></q-btn> ><q-tooltip>Print</q-tooltip></q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>

View file

@ -179,6 +179,11 @@ Vue.component('lnbits-extension-list', {
Vue.component('lnbits-payment-details', { Vue.component('lnbits-payment-details', {
props: ['payment'], props: ['payment'],
data: function () {
return {
LNBITS_DENOMINATION: LNBITS_DENOMINATION
}
},
template: ` template: `
<div class="q-py-md" style="text-align: left"> <div class="q-py-md" style="text-align: left">
<div class="row justify-center q-mb-md"> <div class="row justify-center q-mb-md">

View file

@ -45,9 +45,16 @@ async def test_get_wallet_adminkey(client, adminkey_headers_to):
assert "id" in result assert "id" in result
# check POST /api/v1/payments: empty request # check PUT /api/v1/wallet/newwallet: empty request where admin key is needed
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_post_empty_request(client): async def test_put_empty_request_expected_admin_keys(client):
response = await client.put("/api/v1/wallet/newwallet")
assert response.status_code == 401
# check POST /api/v1/payments: empty request where invoice key is needed
@pytest.mark.asyncio
async def test_post_empty_request_expected_invoice_keys(client):
response = await client.post("/api/v1/payments") response = await client.post("/api/v1/payments")
assert response.status_code == 401 assert response.status_code == 401

Binary file not shown.

View file

@ -19,16 +19,12 @@ env.read_env()
# Change these values as needed # Change these values as needed
sqfolder = "data/" sqfolder = env.str("LNBITS_DATA_FOLDER", default=None)
LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None) LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
if LNBITS_DATABASE_URL is None: if LNBITS_DATABASE_URL is None:
pgdb = "lnbits" print("missing LNBITS_DATABASE_URL")
pguser = "lnbits" sys.exit(1)
pgpswd = "postgres"
pghost = "localhost"
pgport = "5432"
pgschema = ""
else: else:
# parse postgres://lnbits:postgres@localhost:5432/lnbits # parse postgres://lnbits:postgres@localhost:5432/lnbits
pgdb = LNBITS_DATABASE_URL.split("/")[-1] pgdb = LNBITS_DATABASE_URL.split("/")[-1]