mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-24 14:51:05 +01:00
commit
ca60893701
43 changed files with 1756 additions and 89 deletions
58
.github/workflows/on-push.yml
vendored
Normal file
58
.github/workflows/on-push.yml
vendored
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
name: Docker build on push
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
name: Build and push lnbits image
|
||||||
|
steps:
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Checkout project
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
id: qemu
|
||||||
|
|
||||||
|
- name: Setup Docker buildx action
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
id: buildx
|
||||||
|
|
||||||
|
- name: Show available Docker buildx platforms
|
||||||
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: cache
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
- name: Run Docker buildx against commit hash
|
||||||
|
run: |
|
||||||
|
docker buildx build \
|
||||||
|
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||||
|
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||||
|
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||||
|
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits:${GITHUB_SHA:0:7} \
|
||||||
|
--output "type=registry" ./
|
||||||
|
|
||||||
|
- name: Run Docker buildx against latest
|
||||||
|
run: |
|
||||||
|
docker buildx build \
|
||||||
|
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||||
|
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||||
|
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||||
|
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits:latest \
|
||||||
|
--output "type=registry" ./
|
48
Dockerfile
48
Dockerfile
|
@ -1,8 +1,48 @@
|
||||||
FROM python:3.7-slim
|
# Build image
|
||||||
|
FROM python:3.7-slim as builder
|
||||||
|
|
||||||
|
# Setup virtualenv
|
||||||
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
|
RUN python -m venv $VIRTUAL_ENV
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
# Install build deps
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y --no-install-recommends build-essential
|
||||||
|
|
||||||
|
# Install runtime deps
|
||||||
|
COPY requirements.txt /tmp/requirements.txt
|
||||||
|
RUN pip install -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
# Install c-lightning specific deps
|
||||||
|
RUN pip install pylightning
|
||||||
|
|
||||||
|
# Install LND specific deps
|
||||||
|
RUN pip install lndgrpc purerpc
|
||||||
|
|
||||||
|
# Production image
|
||||||
|
FROM python:3.7-slim as lnbits
|
||||||
|
|
||||||
|
# Run as non-root
|
||||||
|
USER 1000:1000
|
||||||
|
|
||||||
|
# Copy over virtualenv
|
||||||
|
ENV VIRTUAL_ENV="/opt/venv"
|
||||||
|
COPY --from=builder --chown=1000:1000 $VIRTUAL_ENV $VIRTUAL_ENV
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
# Setup Quart
|
||||||
|
ENV QUART_APP="lnbits.app:create_app()"
|
||||||
|
ENV QUART_ENV="development"
|
||||||
|
ENV QUART_DEBUG="true"
|
||||||
|
|
||||||
|
# App
|
||||||
|
ENV LNBITS_BIND="0.0.0.0:5000"
|
||||||
|
|
||||||
|
# Copy in app source
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY requirements.txt /app/
|
COPY --chown=1000:1000 lnbits /app/lnbits
|
||||||
RUN pip install --no-cache-dir -q -r requirements.txt
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD quart assets && quart migrate && hypercorn -k trio --bind $LNBITS_BIND 'lnbits.app:create_app()'
|
||||||
|
|
|
@ -22,7 +22,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an
|
||||||
* Fallback wallet for the LNURL scheme
|
* 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, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily.
|
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily.
|
||||||
|
|
||||||
See [lnbits.org](https://lnbits.org) for more detailed documentation.
|
See [lnbits.org](https://lnbits.org) for more detailed documentation.
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ Wallets can be easily generated and given out to people at events (one click mul
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Tip me
|
## Tip us
|
||||||
|
|
||||||
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@ $ pipenv shell
|
||||||
$ pipenv install --dev
|
$ pipenv install --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If any of the modules fails to install, try checking and upgrading your setupTool module.
|
||||||
|
`pip install -U setuptools`
|
||||||
|
|
||||||
If you wish to use a version of Python higher than 3.7:
|
If you wish to use a version of Python higher than 3.7:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -253,13 +253,14 @@ async def create_payment(
|
||||||
preimage: Optional[str] = None,
|
preimage: Optional[str] = None,
|
||||||
pending: bool = True,
|
pending: bool = True,
|
||||||
extra: Optional[Dict] = None,
|
extra: Optional[Dict] = None,
|
||||||
|
webhook: Optional[str] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO apipayments
|
INSERT INTO apipayments
|
||||||
(wallet, checking_id, bolt11, hash, preimage,
|
(wallet, checking_id, bolt11, hash, preimage,
|
||||||
amount, pending, memo, fee, extra)
|
amount, pending, memo, fee, extra, webhook)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
wallet_id,
|
wallet_id,
|
||||||
|
@ -272,6 +273,7 @@ async def create_payment(
|
||||||
memo,
|
memo,
|
||||||
fee,
|
fee,
|
||||||
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None,
|
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None,
|
||||||
|
webhook,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -120,3 +120,13 @@ async def m002_add_fields_to_apipayments(db):
|
||||||
# catching errors like this won't be necessary in anymore now that we
|
# catching errors like this won't be necessary in anymore now that we
|
||||||
# keep track of db versions so no migration ever runs twice.
|
# keep track of db versions so no migration ever runs twice.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def m003_add_invoice_webhook(db):
|
||||||
|
"""
|
||||||
|
Special column for webhook endpoints that can be assigned
|
||||||
|
to each different invoice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook TEXT")
|
||||||
|
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT")
|
||||||
|
|
|
@ -84,6 +84,8 @@ class Payment(NamedTuple):
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
extra: Dict
|
extra: Dict
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
|
webhook: str
|
||||||
|
webhook_status: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row):
|
def from_row(cls, row: Row):
|
||||||
|
@ -99,6 +101,8 @@ class Payment(NamedTuple):
|
||||||
memo=row["memo"],
|
memo=row["memo"],
|
||||||
time=row["time"],
|
time=row["time"],
|
||||||
wallet_id=row["wallet"],
|
wallet_id=row["wallet"],
|
||||||
|
webhook=row["webhook"],
|
||||||
|
webhook_status=row["webhook_status"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -28,6 +28,7 @@ async def create_invoice(
|
||||||
memo: str,
|
memo: str,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
extra: Optional[Dict] = None,
|
extra: Optional[Dict] = None,
|
||||||
|
webhook: Optional[str] = None,
|
||||||
) -> Tuple[str, str]:
|
) -> Tuple[str, str]:
|
||||||
await db.begin()
|
await db.begin()
|
||||||
invoice_memo = None if description_hash else memo
|
invoice_memo = None if description_hash else memo
|
||||||
|
@ -50,6 +51,7 @@ async def create_invoice(
|
||||||
amount=amount_msat,
|
amount=amount_msat,
|
||||||
memo=storeable_memo,
|
memo=storeable_memo,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
|
webhook=webhook,
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
@ -137,10 +139,12 @@ async def pay_invoice(
|
||||||
**payment_kwargs,
|
**payment_kwargs,
|
||||||
)
|
)
|
||||||
await delete_payment(temp_id)
|
await delete_payment(temp_id)
|
||||||
|
await db.commit()
|
||||||
else:
|
else:
|
||||||
|
await delete_payment(temp_id)
|
||||||
|
await db.commit()
|
||||||
raise Exception(payment.error_message or "Failed to pay_invoice on backend.")
|
raise Exception(payment.error_message or "Failed to pay_invoice on backend.")
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return invoice.payment_hash
|
return invoice.payment_hash
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import trio # type: ignore
|
import trio # type: ignore
|
||||||
|
import httpx
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
from . import db
|
||||||
|
from .models import Payment
|
||||||
|
|
||||||
sse_listeners: List[trio.MemorySendChannel] = []
|
sse_listeners: List[trio.MemorySendChannel] = []
|
||||||
|
|
||||||
|
@ -14,9 +17,42 @@ async def register_listeners():
|
||||||
|
|
||||||
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
async for payment in invoice_paid_chan:
|
async for payment in invoice_paid_chan:
|
||||||
|
# send information to sse channel
|
||||||
|
await dispatch_sse(payment)
|
||||||
|
|
||||||
|
# dispatch webhook
|
||||||
|
if payment.webhook and not payment.webhook_status:
|
||||||
|
await dispatch_webhook(payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def dispatch_sse(payment: Payment):
|
||||||
for send_channel in sse_listeners:
|
for send_channel in sse_listeners:
|
||||||
try:
|
try:
|
||||||
send_channel.send_nowait(payment)
|
send_channel.send_nowait(payment)
|
||||||
except trio.WouldBlock:
|
except trio.WouldBlock:
|
||||||
print("removing sse listener", send_channel)
|
print("removing sse listener", send_channel)
|
||||||
sse_listeners.remove(send_channel)
|
sse_listeners.remove(send_channel)
|
||||||
|
|
||||||
|
|
||||||
|
async def dispatch_webhook(payment: Payment):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
data = payment._asdict()
|
||||||
|
try:
|
||||||
|
r = await client.post(
|
||||||
|
payment.webhook,
|
||||||
|
json=data,
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
await mark_webhook_sent(payment, r.status_code)
|
||||||
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
|
await mark_webhook_sent(payment, -1)
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE apipayments SET webhook_status = ?
|
||||||
|
WHERE hash = ?
|
||||||
|
""",
|
||||||
|
(status, payment.payment_hash),
|
||||||
|
)
|
||||||
|
|
|
@ -55,8 +55,9 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false,
|
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false,
|
||||||
"amount": <int>, "memo": <string>}' -H "X-Api-Key:
|
"amount": <int>, "memo": <string>, "webhook":
|
||||||
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
|
<url:string>}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
|
||||||
|
"Content-type: application/json"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
@ -219,9 +219,6 @@
|
||||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-btn flat color="grey" @click="exportCSV" class="float-right"
|
|
||||||
>Renew keys</q-btn
|
|
||||||
>
|
|
||||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">LNbits wallet</h6>
|
<h6 class="text-subtitle1 q-mt-none q-mb-sm">LNbits wallet</h6>
|
||||||
<strong>Wallet name: </strong><em>{{ wallet.name }}</em><br />
|
<strong>Wallet name: </strong><em>{{ wallet.name }}</em><br />
|
||||||
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br />
|
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br />
|
||||||
|
@ -233,6 +230,22 @@
|
||||||
<q-list>
|
<q-list>
|
||||||
{% include "core/_api_docs.html" %}
|
{% include "core/_api_docs.html" %}
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="settings_cell"
|
||||||
|
label="Export to Phone with QR Code"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<p>This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.</p>
|
||||||
|
<qrcode
|
||||||
|
:value="'{{request.url_root}}'+'wallet?usr={{user.id}}&wal={{wallet.id}}'"
|
||||||
|
:options="{width:240}"
|
||||||
|
></qrcode>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="remove_circle"
|
icon="remove_circle"
|
||||||
|
|
|
@ -51,6 +51,8 @@ async def api_payments():
|
||||||
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
|
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
|
||||||
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
||||||
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
|
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
|
||||||
|
"extra": {"type": "dict", "nullable": True, "required": False},
|
||||||
|
"webhook": {"type": "string", "empty": False, "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_payments_create_invoice():
|
async def api_payments_create_invoice():
|
||||||
|
@ -63,7 +65,12 @@ async def api_payments_create_invoice():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash
|
wallet_id=g.wallet.id,
|
||||||
|
amount=g.data["amount"],
|
||||||
|
memo=memo,
|
||||||
|
description_hash=description_hash,
|
||||||
|
extra=g.data.get("extra"),
|
||||||
|
webhook=g.data.get("webhook"),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
await db.rollback()
|
await db.rollback()
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# async def m001_initial(db):
|
||||||
|
|
||||||
|
# await db.execute(
|
||||||
|
# """
|
||||||
|
# CREATE TABLE IF NOT EXISTS example (
|
||||||
|
# id TEXT PRIMARY KEY,
|
||||||
|
# wallet TEXT NOT NULL,
|
||||||
|
# time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
# );
|
||||||
|
# """
|
||||||
|
# )
|
11
lnbits/extensions/example/models.py
Normal file
11
lnbits/extensions/example/models.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# from sqlite3 import Row
|
||||||
|
# from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
|
# class Example(NamedTuple):
|
||||||
|
# id: str
|
||||||
|
# wallet: str
|
||||||
|
#
|
||||||
|
# @classmethod
|
||||||
|
# def from_row(cls, row: Row) -> "Example":
|
||||||
|
# return cls(**dict(row))
|
|
@ -21,8 +21,8 @@ async def api_example():
|
||||||
"""Try to add descriptions for others."""
|
"""Try to add descriptions for others."""
|
||||||
tools = [
|
tools = [
|
||||||
{
|
{
|
||||||
"name": "Flask",
|
"name": "Quart",
|
||||||
"url": "https://flask.palletsprojects.com/",
|
"url": "https://pgjones.gitlab.io/quart/",
|
||||||
"language": "Python",
|
"language": "Python",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,6 +6,7 @@ from . import db
|
||||||
from .models import Tickets, Forms
|
from .models import Tickets, Forms
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
async def create_ticket(
|
async def create_ticket(
|
||||||
payment_hash: str,
|
payment_hash: str,
|
||||||
wallet: str,
|
wallet: str,
|
||||||
|
@ -54,16 +55,12 @@ async def set_ticket_paid(payment_hash: str) -> Tickets:
|
||||||
)
|
)
|
||||||
|
|
||||||
ticket = await get_ticket(payment_hash)
|
ticket = await get_ticket(payment_hash)
|
||||||
|
if formdata.webhook:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
formdata.webhook,
|
formdata.webhook,
|
||||||
json={
|
json={"form": ticket.form, "name": ticket.name, "email": ticket.email, "content": ticket.ltext},
|
||||||
"form": ticket.form,
|
|
||||||
"name": ticket.name,
|
|
||||||
"email": ticket.email,
|
|
||||||
"content": ticket.ltext
|
|
||||||
},
|
|
||||||
timeout=40,
|
timeout=40,
|
||||||
)
|
)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
|
@ -95,7 +92,9 @@ async def delete_ticket(ticket_id: str) -> None:
|
||||||
# FORMS
|
# FORMS
|
||||||
|
|
||||||
|
|
||||||
async def create_form(*, wallet: str, name: str, webhook: Optional[str] = None, description: str, costpword: int) -> Forms:
|
async def create_form(
|
||||||
|
*, wallet: str, name: str, webhook: Optional[str] = None, description: str, costpword: int
|
||||||
|
) -> Forms:
|
||||||
form_id = urlsafe_short_hash()
|
form_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -102,7 +102,6 @@ async def m003_changed(db):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
for row in [list(row) for row in await db.fetchall("SELECT * FROM forms")]:
|
for row in [list(row) for row in await db.fetchall("SELECT * FROM forms")]:
|
||||||
usescsv = ""
|
usescsv = ""
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,7 @@
|
||||||
name: self.formDialog.data.name,
|
name: self.formDialog.data.name,
|
||||||
email: self.formDialog.data.email,
|
email: self.formDialog.data.email,
|
||||||
ltext: self.formDialog.data.text,
|
ltext: self.formDialog.data.text,
|
||||||
sats: self.formDialog.data.sats,
|
sats: self.formDialog.data.sats
|
||||||
})
|
})
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.paymentReq = response.data.payment_request
|
self.paymentReq = response.data.payment_request
|
||||||
|
@ -172,16 +172,14 @@
|
||||||
}
|
}
|
||||||
dismissMsg()
|
dismissMsg()
|
||||||
|
|
||||||
|
|
||||||
self.formDialog.data.name = ''
|
self.formDialog.data.name = ''
|
||||||
self.formDialog.data.email = ''
|
self.formDialog.data.email = ''
|
||||||
self.formDialog.data.text = ''
|
self.formDialog.data.text = ''
|
||||||
self.$q.notify({
|
self.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Sent, thank you!',
|
message: 'Sent, thank you!',
|
||||||
icon: 'thumb_up',
|
icon: 'thumb_up'
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
|
|
@ -252,7 +252,12 @@
|
||||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||||
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
|
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
|
||||||
{name: 'webhook', align: 'left', label: 'Webhook', field: 'webhook'},
|
{
|
||||||
|
name: 'webhook',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Webhook',
|
||||||
|
field: 'webhook'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
<code>[<pay_link_object>, ...]</code>
|
<code>[<pay_link_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root }}lnurlp/api/v1/links -H "X-Api-Key:
|
>curl -X GET {{ request.url_root }}api/v0/links -H "X-Api-Key: {{
|
||||||
{{ g.user.wallets[0].inkey }}"
|
g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
<code>{"lnurl": <string>}</code>
|
<code>{"lnurl": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root }}lnurlp/api/v1/links/<pay_id>
|
>curl -X GET {{ request.url_root }}api/v1/links/<pay_id> -H
|
||||||
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
@ -63,10 +63,9 @@
|
||||||
<code>{"lnurl": <string>}</code>
|
<code>{"lnurl": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}lnurlp/api/v1/links -d
|
>curl -X POST {{ request.url_root }}api/v1/links -d '{"description":
|
||||||
'{"description": <string>, "amount": <integer>}' -H
|
<string>, "amount": <integer>}' -H "Content-type:
|
||||||
"Content-type: application/json" -H "X-Api-Key: {{
|
application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||||
g.user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
@ -93,8 +92,8 @@
|
||||||
<code>{"lnurl": <string>}</code>
|
<code>{"lnurl": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X PUT {{ request.url_root }}lnurlp/api/v1/links/<pay_id>
|
>curl -X PUT {{ request.url_root }}api/v1/links/<pay_id> -d
|
||||||
-d '{"description": <string>, "amount": <integer>}' -H
|
'{"description": <string>, "amount": <integer>}' -H
|
||||||
"Content-type: application/json" -H "X-Api-Key: {{
|
"Content-type: application/json" -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].adminkey }}"
|
g.user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
|
@ -120,9 +119,8 @@
|
||||||
<code></code>
|
<code></code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.url_root
|
>curl -X DELETE {{ request.url_root }}api/v1/links/<pay_id> -H
|
||||||
}}lnurlp/api/v1/links/<pay_id> -H "X-Api-Key: {{
|
"X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||||
g.user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<code>[<paywall_object>, ...]</code>
|
<code>[<paywall_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root }}paywall/api/v1/paywalls -H
|
>curl -X GET {{ request.url_root }}api/v1/paywalls -H
|
||||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}paywall/api/v1/paywalls -d
|
>curl -X POST {{ request.url_root }}api/v1/paywalls -d
|
||||||
'{"url": <string>, "memo": <string>, "description":
|
'{"url": <string>, "memo": <string>, "description":
|
||||||
<string>, "amount": <integer>, "remembers":
|
<string>, "amount": <integer>, "remembers":
|
||||||
<boolean>}' -H "Content-type: application/json" -H "X-Api-Key:
|
<boolean>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root
|
>curl -X POST {{ request.url_root
|
||||||
}}paywall/api/v1/paywalls/<paywall_id>/invoice -d '{"amount":
|
}}api/v1/paywalls/<paywall_id>/invoice -d '{"amount":
|
||||||
<integer>}' -H "Content-type: application/json"
|
<integer>}' -H "Content-type: application/json"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root
|
>curl -X POST {{ request.url_root
|
||||||
}}paywall/api/v1/paywalls/<paywall_id>/check_invoice -d
|
}}api/v1/paywalls/<paywall_id>/check_invoice -d
|
||||||
'{"payment_hash": <string>}' -H "Content-type: application/json"
|
'{"payment_hash": <string>}' -H "Content-type: application/json"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -138,7 +138,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.url_root
|
>curl -X DELETE {{ request.url_root
|
||||||
}}paywall/api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{
|
}}api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].adminkey }}"
|
g.user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
54
lnbits/extensions/subdomains/README.md
Normal file
54
lnbits/extensions/subdomains/README.md
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<h1>Subdomains Extension</h1>
|
||||||
|
|
||||||
|
So the goal of the extension is to allow the owner of a domain to sell their subdomain to the anyone who is willing to pay some money for it.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Free cloudflare account
|
||||||
|
- Cloudflare as a dns server provider
|
||||||
|
- Cloudflare TOKEN and Cloudflare zone-id where the domain is parked
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Register at cloudflare and setup your domain with them. (Just follow instructions they provide...)
|
||||||
|
2. Change DNS server at your domain registrar to point to cloudflare's
|
||||||
|
3. Get Cloudflare zoneID for your domain
|
||||||
|
<img src="https://i.imgur.com/xOgapHr.png">
|
||||||
|
4. get Cloudflare API TOKEN
|
||||||
|
<img src="https://i.imgur.com/BZbktTy.png">
|
||||||
|
<img src="https://i.imgur.com/YDZpW7D.png">
|
||||||
|
5. Open the lnbits subdomains extension and register your domain with lnbits
|
||||||
|
6. Click on the button in the table to open the public form that was generated for your domain
|
||||||
|
|
||||||
|
- Extension also supports webhooks so you can get notified when someone buys a new domain
|
||||||
|
<img src="https://i.imgur.com/hiauxeR.png">
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- **Domains**
|
||||||
|
- GET /api/v1/domains
|
||||||
|
- POST /api/v1/domains
|
||||||
|
- PUT /api/v1/domains/<domain_id>
|
||||||
|
- DELETE /api/v1/domains/<domain_id>
|
||||||
|
- **Subdomains**
|
||||||
|
- GET /api/v1/subdomains
|
||||||
|
- POST /api/v1/subdomains/<domain_id>
|
||||||
|
- GET /api/v1/subdomains/<payment_hash>
|
||||||
|
- DELETE /api/v1/subdomains/<subdomain_id>
|
||||||
|
|
||||||
|
## Useful
|
||||||
|
|
||||||
|
### Cloudflare
|
||||||
|
|
||||||
|
- Cloudflare offers programmatic subdomain registration... (create new A record)
|
||||||
|
- you can keep your existing domain's registrar, you just have to transfer dns records to the cloudflare (free service)
|
||||||
|
- more information:
|
||||||
|
- https://api.cloudflare.com/#getting-started-requests
|
||||||
|
- API endpoints needed for our project:
|
||||||
|
- https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
|
||||||
|
- https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
|
||||||
|
- https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record
|
||||||
|
- https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
|
||||||
|
- api can be used by providing authorization token OR authorization key
|
||||||
|
- check API Tokens and API Keys : https://api.cloudflare.com/#getting-started-requests
|
||||||
|
- Cloudflare API postman collection: https://support.cloudflare.com/hc/en-us/articles/115002323852-Using-Cloudflare-API-with-Postman-Collections
|
15
lnbits/extensions/subdomains/__init__.py
Normal file
15
lnbits/extensions/subdomains/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from quart import Blueprint
|
||||||
|
from lnbits.db import Database
|
||||||
|
|
||||||
|
db = Database("ext_subdomains")
|
||||||
|
|
||||||
|
subdomains_ext: Blueprint = Blueprint("subdomains", __name__, static_folder="static", template_folder="templates")
|
||||||
|
|
||||||
|
|
||||||
|
from .views_api import * # noqa
|
||||||
|
from .views import * # noqa
|
||||||
|
|
||||||
|
from .tasks import register_listeners
|
||||||
|
from lnbits.tasks import record_async
|
||||||
|
|
||||||
|
subdomains_ext.record(record_async(register_listeners))
|
44
lnbits/extensions/subdomains/cloudflare.py
Normal file
44
lnbits/extensions/subdomains/cloudflare.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from lnbits.extensions.subdomains.models import Domains
|
||||||
|
import httpx, json
|
||||||
|
|
||||||
|
|
||||||
|
async def cloudflare_create_subdomain(domain: Domains, subdomain: str, record_type: str, ip: str):
|
||||||
|
# Call to cloudflare sort of a dry-run - if success delete the domain and wait for payment
|
||||||
|
### SEND REQUEST TO CLOUDFLARE
|
||||||
|
url = "https://api.cloudflare.com/client/v4/zones/" + domain.cf_zone_id + "/dns_records"
|
||||||
|
header = {"Authorization": "Bearer " + domain.cf_token, "Content-Type": "application/json"}
|
||||||
|
aRecord = subdomain + "." + domain.domain
|
||||||
|
cf_response = ""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
r = await client.post(
|
||||||
|
url,
|
||||||
|
headers=header,
|
||||||
|
json={
|
||||||
|
"type": record_type,
|
||||||
|
"name": aRecord,
|
||||||
|
"content": ip,
|
||||||
|
"ttl": 0,
|
||||||
|
"proxed": False,
|
||||||
|
},
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
cf_response = json.loads(r.text)
|
||||||
|
except AssertionError:
|
||||||
|
cf_response = "Error occured"
|
||||||
|
return cf_response
|
||||||
|
|
||||||
|
|
||||||
|
async def cloudflare_deletesubdomain(domain: Domains, domain_id: str):
|
||||||
|
url = "https://api.cloudflare.com/client/v4/zones/" + domain.cf_zone_id + "/dns_records"
|
||||||
|
header = {"Authorization": "Bearer " + domain.cf_token, "Content-Type": "application/json"}
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
r = await client.delete(
|
||||||
|
url + "/" + domain_id,
|
||||||
|
headers=header,
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
cf_response = r.text
|
||||||
|
except AssertionError:
|
||||||
|
cf_response = "Error occured"
|
6
lnbits/extensions/subdomains/config.json
Normal file
6
lnbits/extensions/subdomains/config.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Subdomains",
|
||||||
|
"short_description": "Sell subdomains of your domain",
|
||||||
|
"icon": "domain",
|
||||||
|
"contributors": ["grmkris"]
|
||||||
|
}
|
153
lnbits/extensions/subdomains/crud.py
Normal file
153
lnbits/extensions/subdomains/crud.py
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import Domains, Subdomains
|
||||||
|
from lnbits.extensions import subdomains
|
||||||
|
|
||||||
|
|
||||||
|
async def create_subdomain(
|
||||||
|
payment_hash: str,
|
||||||
|
wallet: str,
|
||||||
|
domain: str,
|
||||||
|
subdomain: str,
|
||||||
|
email: str,
|
||||||
|
ip: str,
|
||||||
|
sats: int,
|
||||||
|
duration: int,
|
||||||
|
record_type: str,
|
||||||
|
) -> Subdomains:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(payment_hash, domain, email, subdomain, ip, wallet, sats, duration, False, record_type),
|
||||||
|
)
|
||||||
|
|
||||||
|
subdomain = await get_subdomain(payment_hash)
|
||||||
|
assert subdomain, "Newly created subdomain couldn't be retrieved"
|
||||||
|
return subdomain
|
||||||
|
|
||||||
|
|
||||||
|
async def set_subdomain_paid(payment_hash: str) -> Subdomains:
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
|
||||||
|
(payment_hash,),
|
||||||
|
)
|
||||||
|
if row[8] == False:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE subdomain
|
||||||
|
SET paid = true
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(payment_hash,),
|
||||||
|
)
|
||||||
|
|
||||||
|
domaindata = await get_domain(row[1])
|
||||||
|
assert domaindata, "Couldn't get domain from paid subdomain"
|
||||||
|
|
||||||
|
amount = domaindata.amountmade + row[8]
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE domain
|
||||||
|
SET amountmade = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(amount, row[1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
subdomain = await get_subdomain(payment_hash)
|
||||||
|
return subdomain
|
||||||
|
|
||||||
|
|
||||||
|
async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
|
||||||
|
(subdomain_id,),
|
||||||
|
)
|
||||||
|
print(row)
|
||||||
|
return Subdomains(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_subdomainBySubdomain(subdomain: str) -> Optional[Subdomains]:
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
|
||||||
|
(subdomain,),
|
||||||
|
)
|
||||||
|
print(row)
|
||||||
|
return Subdomains(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]:
|
||||||
|
if isinstance(wallet_ids, str):
|
||||||
|
wallet_ids = [wallet_ids]
|
||||||
|
|
||||||
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})",
|
||||||
|
(*wallet_ids,),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [Subdomains(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_subdomain(subdomain_id: str) -> None:
|
||||||
|
await db.execute("DELETE FROM subdomain WHERE id = ?", (subdomain_id,))
|
||||||
|
|
||||||
|
|
||||||
|
# Domains
|
||||||
|
|
||||||
|
|
||||||
|
async def create_domain(
|
||||||
|
*,
|
||||||
|
wallet: str,
|
||||||
|
domain: str,
|
||||||
|
cf_token: str,
|
||||||
|
cf_zone_id: str,
|
||||||
|
webhook: Optional[str] = None,
|
||||||
|
description: str,
|
||||||
|
cost: int,
|
||||||
|
allowed_record_types: str,
|
||||||
|
) -> Domains:
|
||||||
|
domain_id = urlsafe_short_hash()
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(domain_id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, 0, allowed_record_types),
|
||||||
|
)
|
||||||
|
|
||||||
|
domain = await get_domain(domain_id)
|
||||||
|
assert domain, "Newly created domain couldn't be retrieved"
|
||||||
|
return domain
|
||||||
|
|
||||||
|
|
||||||
|
async def update_domain(domain_id: str, **kwargs) -> Domains:
|
||||||
|
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||||
|
await db.execute(f"UPDATE domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id))
|
||||||
|
row = await db.fetchone("SELECT * FROM domain WHERE id = ?", (domain_id,))
|
||||||
|
assert row, "Newly updated domain couldn't be retrieved"
|
||||||
|
return Domains(**row)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_domain(domain_id: str) -> Optional[Domains]:
|
||||||
|
row = await db.fetchone("SELECT * FROM domain WHERE id = ?", (domain_id,))
|
||||||
|
return Domains(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]:
|
||||||
|
if isinstance(wallet_ids, str):
|
||||||
|
wallet_ids = [wallet_ids]
|
||||||
|
|
||||||
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
|
rows = await db.fetchall(f"SELECT * FROM domain WHERE wallet IN ({q})", (*wallet_ids,))
|
||||||
|
|
||||||
|
return [Domains(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_domain(domain_id: str) -> None:
|
||||||
|
await db.execute("DELETE FROM domain WHERE id = ?", (domain_id,))
|
37
lnbits/extensions/subdomains/migrations.py
Normal file
37
lnbits/extensions/subdomains/migrations.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
async def m001_initial(db):
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS domain (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
webhook TEXT,
|
||||||
|
cf_token TEXT NOT NULL,
|
||||||
|
cf_zone_id TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
cost INTEGER NOT NULL,
|
||||||
|
amountmade INTEGER NOT NULL,
|
||||||
|
allowed_record_types TEXT NOT NULL,
|
||||||
|
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS subdomain (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
subdomain TEXT NOT NULL,
|
||||||
|
ip TEXT NOT NULL,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
sats INTEGER NOT NULL,
|
||||||
|
duration INTEGER NOT NULL,
|
||||||
|
paid BOOLEAN NOT NULL,
|
||||||
|
record_type TEXT NOT NULL,
|
||||||
|
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
30
lnbits/extensions/subdomains/models.py
Normal file
30
lnbits/extensions/subdomains/models.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
|
class Domains(NamedTuple):
|
||||||
|
id: str
|
||||||
|
wallet: str
|
||||||
|
domain: str
|
||||||
|
cf_token: str
|
||||||
|
cf_zone_id: str
|
||||||
|
webhook: str
|
||||||
|
description: str
|
||||||
|
cost: int
|
||||||
|
amountmade: int
|
||||||
|
time: int
|
||||||
|
allowed_record_types: str
|
||||||
|
|
||||||
|
|
||||||
|
class Subdomains(NamedTuple):
|
||||||
|
id: str
|
||||||
|
wallet: str
|
||||||
|
domain: str
|
||||||
|
domain_name: str
|
||||||
|
subdomain: str
|
||||||
|
email: str
|
||||||
|
ip: str
|
||||||
|
sats: int
|
||||||
|
duration: int
|
||||||
|
paid: bool
|
||||||
|
time: int
|
||||||
|
record_type: str
|
58
lnbits/extensions/subdomains/tasks.py
Normal file
58
lnbits/extensions/subdomains/tasks.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from quart.json import jsonify
|
||||||
|
import trio # type: ignore
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .crud import get_domain, set_subdomain_paid
|
||||||
|
from lnbits.core.crud import get_user, get_wallet
|
||||||
|
from lnbits.core import db as core_db
|
||||||
|
from lnbits.core.models import Payment
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
from .cloudflare import cloudflare_create_subdomain
|
||||||
|
|
||||||
|
|
||||||
|
async def register_listeners():
|
||||||
|
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
|
||||||
|
register_invoice_listener(invoice_paid_chan_send)
|
||||||
|
await wait_for_paid_invoices(invoice_paid_chan_recv)
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
|
async for payment in invoice_paid_chan:
|
||||||
|
await on_invoice_paid(payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
if "lnsubdomain" != payment.extra.get("tag"):
|
||||||
|
# not an lnurlp invoice
|
||||||
|
return
|
||||||
|
|
||||||
|
await payment.set_pending(False)
|
||||||
|
subdomain = await set_subdomain_paid(payment_hash=payment.payment_hash)
|
||||||
|
domain = await get_domain(subdomain.domain)
|
||||||
|
|
||||||
|
### Create subdomain
|
||||||
|
cf_response = cloudflare_create_subdomain(
|
||||||
|
domain=domain, subdomain=subdomain.subdomain, record_type=subdomain.record_type, ip=subdomain.ip
|
||||||
|
)
|
||||||
|
|
||||||
|
### Use webhook to notify about cloudflare registration
|
||||||
|
if domain.webhook:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
r = await client.post(
|
||||||
|
domain.webhook,
|
||||||
|
json={
|
||||||
|
"domain": subdomain.domain_name,
|
||||||
|
"subdomain": subdomain.subdomain,
|
||||||
|
"record_type": subdomain.record_type,
|
||||||
|
"email": subdomain.email,
|
||||||
|
"ip": subdomain.ip,
|
||||||
|
"cost:": str(subdomain.sats) + " sats",
|
||||||
|
"duration": str(subdomain.duration) + " days",
|
||||||
|
"cf_response": cf_response,
|
||||||
|
},
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
except AssertionError:
|
||||||
|
webhook = None
|
|
@ -0,0 +1,23 @@
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="swap_vertical_circle"
|
||||||
|
label="About lnSubdomains"
|
||||||
|
:content-inset-level="0.5"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h5 class="text-subtitle1 q-my-none">
|
||||||
|
lnSubdomains: Get paid sats to sell your subdomains
|
||||||
|
</h5>
|
||||||
|
<p>
|
||||||
|
Charge people for using your subdomain name...<br />
|
||||||
|
Are you the owner of <b>cool-domain.com</b> and want to sell
|
||||||
|
<i>cool-subdomain</i>.<b>cool-domain.com</b>
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
Created by, <a href="https://github.com/grmkris">Kris</a></small
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
221
lnbits/extensions/subdomains/templates/subdomains/display.html
Normal file
221
lnbits/extensions/subdomains/templates/subdomains/display.html
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
{% 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">{{ domain_domain }}</h3>
|
||||||
|
<br />
|
||||||
|
<h5 class="q-my-none">{{ domain_desc }}</h5>
|
||||||
|
<br />
|
||||||
|
<q-form @submit="Invoice()" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.email"
|
||||||
|
type="email"
|
||||||
|
label="Your email (optional, if you want a reply)"
|
||||||
|
></q-input>
|
||||||
|
<q-select
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="formDialog.data.record_type"
|
||||||
|
:options="{{domain_allowed_record_types}}"
|
||||||
|
label="Record type"
|
||||||
|
></q-select>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.subdomain"
|
||||||
|
type="text"
|
||||||
|
label="Subdomain you want"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.ip"
|
||||||
|
type="text"
|
||||||
|
label="Ip of your server"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.duration"
|
||||||
|
type="number"
|
||||||
|
label="Number of days"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<p>
|
||||||
|
Cost per day: {{ domain_cost }} sats<br />
|
||||||
|
{% raw %} Total cost: {{amountSats}} sats {% endraw %}
|
||||||
|
</p>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="deep-purple"
|
||||||
|
:disable="formDialog.data.subdomain == '' || formDialog.data.ip == '' || formDialog.data.duration == ''"
|
||||||
|
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>
|
||||||
|
console.log('{{ domain_cost }}')
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
paymentReq: null,
|
||||||
|
redirectUrl: null,
|
||||||
|
formDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
ip: '',
|
||||||
|
subdomain: '',
|
||||||
|
duration: '',
|
||||||
|
email: '',
|
||||||
|
record_type: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
receive: {
|
||||||
|
show: false,
|
||||||
|
status: 'pending',
|
||||||
|
paymentReq: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
amountSats() {
|
||||||
|
var sats = this.formDialog.data.duration * parseInt('{{ domain_cost }}')
|
||||||
|
this.formDialog.data.sats = sats
|
||||||
|
return sats
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
resetForm: function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.formDialog.data.subdomain = ''
|
||||||
|
this.formDialog.data.email = ''
|
||||||
|
this.formDialog.data.ip = ''
|
||||||
|
this.formDialog.data.duration = ''
|
||||||
|
this.formDialog.data.record_type = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
closeReceiveDialog: function () {
|
||||||
|
var checker = this.receive.paymentChecker
|
||||||
|
dismissMsg()
|
||||||
|
|
||||||
|
clearInterval(paymentChecker)
|
||||||
|
setTimeout(function () {}, 10000)
|
||||||
|
},
|
||||||
|
Invoice: function () {
|
||||||
|
var self = this
|
||||||
|
axios
|
||||||
|
.post('/subdomains/api/v1/subdomains/{{ domain_id }}', {
|
||||||
|
domain: '{{ domain_id }}',
|
||||||
|
subdomain: self.formDialog.data.subdomain,
|
||||||
|
ip: self.formDialog.data.ip,
|
||||||
|
email: self.formDialog.data.email,
|
||||||
|
sats: self.formDialog.data.sats,
|
||||||
|
duration: parseInt(self.formDialog.data.duration),
|
||||||
|
record_type: self.formDialog.data.record_type
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
self.paymentReq = response.data.payment_request
|
||||||
|
self.paymentCheck = response.data.payment_hash
|
||||||
|
|
||||||
|
dismissMsg = self.$q.notify({
|
||||||
|
timeout: 0,
|
||||||
|
message: 'Waiting for payment...'
|
||||||
|
})
|
||||||
|
|
||||||
|
self.receive = {
|
||||||
|
show: true,
|
||||||
|
status: 'pending',
|
||||||
|
paymentReq: self.paymentReq
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentChecker = setInterval(function () {
|
||||||
|
axios
|
||||||
|
.get('/subdomains/api/v1/subdomains/' + self.paymentCheck)
|
||||||
|
.then(function (res) {
|
||||||
|
console.log(res.data)
|
||||||
|
if (res.data.paid) {
|
||||||
|
clearInterval(paymentChecker)
|
||||||
|
self.receive = {
|
||||||
|
show: false,
|
||||||
|
status: 'complete',
|
||||||
|
paymentReq: null
|
||||||
|
}
|
||||||
|
dismissMsg()
|
||||||
|
|
||||||
|
console.log(self.formDialog)
|
||||||
|
self.formDialog.data.subdomain = ''
|
||||||
|
self.formDialog.data.email = ''
|
||||||
|
self.formDialog.data.ip = ''
|
||||||
|
self.formDialog.data.duration = ''
|
||||||
|
self.formDialog.data.record_type = ''
|
||||||
|
self.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Sent, thank you!',
|
||||||
|
icon: 'thumb_up'
|
||||||
|
})
|
||||||
|
console.log('END')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
545
lnbits/extensions/subdomains/templates/subdomains/index.html
Normal file
545
lnbits/extensions/subdomains/templates/subdomains/index.html
Normal file
|
@ -0,0 +1,545 @@
|
||||||
|
{% 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="domainDialog.show = true"
|
||||||
|
>New Domain</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">Domains</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn flat color="grey" @click="exportDomainsCSV"
|
||||||
|
>Export to CSV</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:data="domains"
|
||||||
|
row-key="id"
|
||||||
|
:columns="domainsTable.columns"
|
||||||
|
:pagination.sync="domainsTable.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="updateDomainDialog(props.row.id)"
|
||||||
|
icon="edit"
|
||||||
|
color="light-blue"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteDomain(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">Subdomains</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn flat color="grey" @click="exportSubdomainsCSV"
|
||||||
|
>Export to CSV</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:data="subdomains"
|
||||||
|
row-key="id"
|
||||||
|
:columns="subdomainsTable.columns"
|
||||||
|
:pagination.sync="subdomainsTable.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" v-if="props.row.paid">
|
||||||
|
<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="deleteSubdomain(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<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 Subdomain extension</h6>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-list> {% include "subdomains/_api_docs.html" %} </q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-dialog v-model="domainDialog.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="domainDialog.data.wallet"
|
||||||
|
:options="g.user.walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
<q-select
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="domainDialog.data.allowed_record_types"
|
||||||
|
multiple
|
||||||
|
:options="dnsRecordTypes"
|
||||||
|
label="Allowed record types"
|
||||||
|
></q-select>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model.trim="domainDialog.data.domain"
|
||||||
|
type="text"
|
||||||
|
label="Domain name "
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="domainDialog.data.cf_token"
|
||||||
|
type="text"
|
||||||
|
label="Cloudflare API token"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="domainDialog.data.cf_zone_id"
|
||||||
|
type="text"
|
||||||
|
label="Cloudflare Zone Id"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="domainDialog.data.webhook"
|
||||||
|
type="text"
|
||||||
|
label="Webhook (optional)"
|
||||||
|
hint="A URL to be called whenever this link receives a payment."
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="domainDialog.data.description"
|
||||||
|
type="textarea"
|
||||||
|
label="Description "
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="domainDialog.data.cost"
|
||||||
|
type="number"
|
||||||
|
label="Amount per day in satoshis"
|
||||||
|
></q-input>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
v-if="domainDialog.data.id"
|
||||||
|
unelevated
|
||||||
|
color="deep-purple"
|
||||||
|
type="submit"
|
||||||
|
>Update Form</q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
unelevated
|
||||||
|
color="deep-purple"
|
||||||
|
:disable="domainDialog.data.cost == null || domainDialog.data.cost < 0 || domainDialog.data.domain == null"
|
||||||
|
type="submit"
|
||||||
|
>Create Domain</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 mapLNDomain = function (obj) {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
obj.displayUrl = ['/subdomains/', obj.id].join('')
|
||||||
|
console.log(obj)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
domains: [],
|
||||||
|
subdomains: [],
|
||||||
|
dnsRecordTypes: [
|
||||||
|
'A',
|
||||||
|
'AAAA',
|
||||||
|
'CNAME',
|
||||||
|
'HTTPS',
|
||||||
|
'TXT',
|
||||||
|
'SRV',
|
||||||
|
'LOC',
|
||||||
|
'MX',
|
||||||
|
'NS',
|
||||||
|
'SPF',
|
||||||
|
'CERT',
|
||||||
|
'DNSKEY',
|
||||||
|
'DS',
|
||||||
|
'NAPTR',
|
||||||
|
'SMIMEA',
|
||||||
|
'SSHFP',
|
||||||
|
'SVCB',
|
||||||
|
'TLSA',
|
||||||
|
'URI'
|
||||||
|
],
|
||||||
|
domainsTable: {
|
||||||
|
columns: [
|
||||||
|
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||||
|
{
|
||||||
|
name: 'domain',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Domain name',
|
||||||
|
field: 'domain'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'allowed_record_types',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Allowed record types',
|
||||||
|
field: 'allowed_record_types'
|
||||||
|
},
|
||||||
|
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
|
||||||
|
{
|
||||||
|
name: 'webhook',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Webhook',
|
||||||
|
field: 'webhook'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Description',
|
||||||
|
field: 'description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cost',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Cost Per Day',
|
||||||
|
field: 'cost'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subdomainsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'subdomain',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Subdomain name',
|
||||||
|
field: 'subdomain'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'domain',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Domain name',
|
||||||
|
field: 'domain_name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'record_type',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Record type',
|
||||||
|
field: 'record_type'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Email',
|
||||||
|
field: 'email'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ip',
|
||||||
|
align: 'left',
|
||||||
|
label: 'IP address',
|
||||||
|
field: 'ip'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sats',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Sats paid',
|
||||||
|
field: 'sats'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duration',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Duration in days',
|
||||||
|
field: 'duration'
|
||||||
|
},
|
||||||
|
{name: 'id', align: 'left', label: 'ID', field: 'id'}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
domainDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSubdomains: function () {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/subdomains/api/v1/subdomains?all_wallets',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.subdomains = response.data.map(function (obj) {
|
||||||
|
return mapLNDomain(obj)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteSubdomain: function (subdomainId) {
|
||||||
|
var self = this
|
||||||
|
var subdomains = _.findWhere(this.subdomains, {id: subdomainId})
|
||||||
|
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this subdomain')
|
||||||
|
.onOk(function () {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/subdomain/api/v1/subdomains/' + subdomainId,
|
||||||
|
_.findWhere(self.g.user.wallets, {id: subdomains.wallet}).inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.subdomains = _.reject(self.subdomains, function (obj) {
|
||||||
|
return obj.id == subdomainId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportSubdomainsCSV: function () {
|
||||||
|
LNbits.utils.exportCSV(this.subdomainsTable.columns, this.subdomains)
|
||||||
|
},
|
||||||
|
|
||||||
|
getDomains: function () {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/subdomains/api/v1/domains?all_wallets',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.domains = response.data.map(function (obj) {
|
||||||
|
return mapLNDomain(obj)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendFormData: function () {
|
||||||
|
var wallet = _.findWhere(this.g.user.wallets, {
|
||||||
|
id: this.domainDialog.data.wallet
|
||||||
|
})
|
||||||
|
var data = this.domainDialog.data
|
||||||
|
data.allowed_record_types = data.allowed_record_types.join(', ')
|
||||||
|
console.log(this.domainDialog)
|
||||||
|
if (data.id) {
|
||||||
|
this.updateDomain(wallet, data)
|
||||||
|
} else {
|
||||||
|
this.createDomain(wallet, data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createDomain: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request('POST', '/subdomains/api/v1/domains', wallet.inkey, data)
|
||||||
|
.then(function (response) {
|
||||||
|
self.domains.push(mapLNDomain(response.data))
|
||||||
|
self.domainDialog.show = false
|
||||||
|
self.domainDialog.data = {}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateDomainDialog: function (formId) {
|
||||||
|
var link = _.findWhere(this.domains, {id: formId})
|
||||||
|
console.log(link.id)
|
||||||
|
this.domainDialog.data = _.clone(link)
|
||||||
|
this.domainDialog.data.allowed_record_types = link.allowed_record_types.split(
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
this.domainDialog.show = true
|
||||||
|
},
|
||||||
|
updateDomain: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
'/subdomains/api/v1/domains/' + data.id,
|
||||||
|
wallet.inkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.domains = _.reject(self.domains, function (obj) {
|
||||||
|
return obj.id == data.id
|
||||||
|
})
|
||||||
|
self.domains.push(mapLNDomain(response.data))
|
||||||
|
self.domainDialog.show = false
|
||||||
|
self.domainDialog.data = {}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteDomain: function (domainId) {
|
||||||
|
var self = this
|
||||||
|
var domains = _.findWhere(this.domains, {id: domainId})
|
||||||
|
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this domain link?')
|
||||||
|
.onOk(function () {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/subdomains/api/v1/domains/' + domainId,
|
||||||
|
_.findWhere(self.g.user.wallets, {id: domains.wallet}).inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.domains = _.reject(self.domains, function (obj) {
|
||||||
|
return obj.id == domainId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportDomainsCSV: function () {
|
||||||
|
LNbits.utils.exportCSV(this.domainsTable.columns, this.domains)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
if (this.g.user.wallets.length) {
|
||||||
|
this.getDomains()
|
||||||
|
this.getSubdomains()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
36
lnbits/extensions/subdomains/util.py
Normal file
36
lnbits/extensions/subdomains/util.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from lnbits.extensions.subdomains.models import Subdomains
|
||||||
|
|
||||||
|
# Python3 program to validate
|
||||||
|
# domain name
|
||||||
|
# using regular expression
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
|
||||||
|
# Function to validate domain name.
|
||||||
|
def isValidDomain(str):
|
||||||
|
# Regex to check valid
|
||||||
|
# domain name.
|
||||||
|
regex = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}"
|
||||||
|
# Compile the ReGex
|
||||||
|
p = re.compile(regex)
|
||||||
|
|
||||||
|
# If the string is empty
|
||||||
|
# return false
|
||||||
|
if str == None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Return if the string
|
||||||
|
# matched the ReGex
|
||||||
|
if re.search(p, str):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Function to validate IP address
|
||||||
|
def isvalidIPAddress(str):
|
||||||
|
try:
|
||||||
|
socket.inet_aton(str)
|
||||||
|
return True
|
||||||
|
except socket.error:
|
||||||
|
return False
|
31
lnbits/extensions/subdomains/views.py
Normal file
31
lnbits/extensions/subdomains/views.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
from quart import g, abort, render_template
|
||||||
|
|
||||||
|
from lnbits.decorators import check_user_exists, validate_uuids
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from . import subdomains_ext
|
||||||
|
from .crud import get_domain
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/")
|
||||||
|
@validate_uuids(["usr"], required=True)
|
||||||
|
@check_user_exists()
|
||||||
|
async def index():
|
||||||
|
return await render_template("subdomains/index.html", user=g.user)
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/<domain_id>")
|
||||||
|
async def display(domain_id):
|
||||||
|
domain = await get_domain(domain_id)
|
||||||
|
if not domain:
|
||||||
|
abort(HTTPStatus.NOT_FOUND, "Domain does not exist.")
|
||||||
|
allowed_records = domain.allowed_record_types.replace('"', "").replace(" ", "").split(",")
|
||||||
|
print(allowed_records)
|
||||||
|
return await render_template(
|
||||||
|
"subdomains/display.html",
|
||||||
|
domain_id=domain.id,
|
||||||
|
domain_domain=domain.domain,
|
||||||
|
domain_desc=domain.description,
|
||||||
|
domain_cost=domain.cost,
|
||||||
|
domain_allowed_record_types=allowed_records,
|
||||||
|
)
|
191
lnbits/extensions/subdomains/views_api.py
Normal file
191
lnbits/extensions/subdomains/views_api.py
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import re
|
||||||
|
from quart import g, jsonify, request
|
||||||
|
from http import HTTPStatus
|
||||||
|
from lnbits.core import crud
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from lnbits.core.crud import get_user, get_wallet
|
||||||
|
from lnbits.core.services import create_invoice, check_invoice_status
|
||||||
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
|
|
||||||
|
from .util import isValidDomain, isvalidIPAddress
|
||||||
|
from . import subdomains_ext
|
||||||
|
from .crud import (
|
||||||
|
create_subdomain,
|
||||||
|
get_subdomain,
|
||||||
|
get_subdomains,
|
||||||
|
delete_subdomain,
|
||||||
|
create_domain,
|
||||||
|
update_domain,
|
||||||
|
get_domain,
|
||||||
|
get_domains,
|
||||||
|
delete_domain,
|
||||||
|
get_subdomainBySubdomain,
|
||||||
|
)
|
||||||
|
from .cloudflare import cloudflare_create_subdomain, cloudflare_deletesubdomain
|
||||||
|
|
||||||
|
|
||||||
|
# domainS
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/api/v1/domains", methods=["GET"])
|
||||||
|
@api_check_wallet_key("invoice")
|
||||||
|
async def api_domains():
|
||||||
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
|
if "all_wallets" in request.args:
|
||||||
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
|
|
||||||
|
return jsonify([domain._asdict() for domain in await get_domains(wallet_ids)]), HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/api/v1/domains", methods=["POST"])
|
||||||
|
@subdomains_ext.route("/api/v1/domains/<domain_id>", methods=["PUT"])
|
||||||
|
@api_check_wallet_key("invoice")
|
||||||
|
@api_validate_post_request(
|
||||||
|
schema={
|
||||||
|
"wallet": {"type": "string", "empty": False, "required": True},
|
||||||
|
"domain": {"type": "string", "empty": False, "required": True},
|
||||||
|
"cf_token": {"type": "string", "empty": False, "required": True},
|
||||||
|
"cf_zone_id": {"type": "string", "empty": False, "required": True},
|
||||||
|
"webhook": {"type": "string", "empty": False, "required": False},
|
||||||
|
"description": {"type": "string", "min": 0, "required": True},
|
||||||
|
"cost": {"type": "integer", "min": 0, "required": True},
|
||||||
|
"allowed_record_types": {"type": "string", "required": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def api_domain_create(domain_id=None):
|
||||||
|
if domain_id:
|
||||||
|
domain = await get_domain(domain_id)
|
||||||
|
|
||||||
|
if not domain:
|
||||||
|
return jsonify({"message": "domain does not exist."}), HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
if domain.wallet != g.wallet.id:
|
||||||
|
return jsonify({"message": "Not your domain."}), HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
|
domain = await update_domain(domain_id, **g.data)
|
||||||
|
else:
|
||||||
|
domain = await create_domain(**g.data)
|
||||||
|
return jsonify(domain._asdict()), HTTPStatus.CREATED
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/api/v1/domains/<domain_id>", methods=["DELETE"])
|
||||||
|
@api_check_wallet_key("invoice")
|
||||||
|
async def api_domain_delete(domain_id):
|
||||||
|
domain = await get_domain(domain_id)
|
||||||
|
|
||||||
|
if not domain:
|
||||||
|
return jsonify({"message": "domain does not exist."}), HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
if domain.wallet != g.wallet.id:
|
||||||
|
return jsonify({"message": "Not your domain."}), HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
|
await delete_domain(domain_id)
|
||||||
|
|
||||||
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
#########subdomains##########
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/api/v1/subdomains", methods=["GET"])
|
||||||
|
@api_check_wallet_key("invoice")
|
||||||
|
async def api_subdomains():
|
||||||
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
|
if "all_wallets" in request.args:
|
||||||
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
|
|
||||||
|
return jsonify([domain._asdict() for domain in await get_subdomains(wallet_ids)]), HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/api/v1/subdomains/<domain_id>", methods=["POST"])
|
||||||
|
@api_validate_post_request(
|
||||||
|
schema={
|
||||||
|
"domain": {"type": "string", "empty": False, "required": True},
|
||||||
|
"subdomain": {"type": "string", "empty": False, "required": True},
|
||||||
|
"email": {"type": "string", "empty": True, "required": True},
|
||||||
|
"ip": {"type": "string", "empty": False, "required": True},
|
||||||
|
"sats": {"type": "integer", "min": 0, "required": True},
|
||||||
|
"duration": {"type": "integer", "empty": False, "required": True},
|
||||||
|
"record_type": {"type": "string", "empty": False, "required": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def api_subdomain_make_subdomain(domain_id):
|
||||||
|
domain = await get_domain(domain_id)
|
||||||
|
|
||||||
|
# If the request is coming for the non-existant domain
|
||||||
|
if not domain:
|
||||||
|
return jsonify({"message": "LNsubdomain does not exist."}), HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
## If record_type is not one of the allowed ones reject the request
|
||||||
|
if g.data["record_type"] not in domain.allowed_record_types:
|
||||||
|
return jsonify({"message": g.data["record_type"] + "Not a valid record"}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
## If domain already exist in our database reject it
|
||||||
|
if await get_subdomainBySubdomain(g.data["subdomain"]) is not None:
|
||||||
|
return (
|
||||||
|
jsonify({"message": g.data["subdomain"] + "." + domain.domain + " domain already taken"}),
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
## Dry run cloudflare... (create and if create is sucessful delete it)
|
||||||
|
cf_response = await cloudflare_create_subdomain(
|
||||||
|
domain=domain, subdomain=g.data["subdomain"], record_type=g.data["record_type"], ip=g.data["ip"]
|
||||||
|
)
|
||||||
|
if cf_response["success"] == True:
|
||||||
|
cloudflare_deletesubdomain(domain=domain, domain_id=cf_response["result"]["id"])
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
jsonify({"message": "Problem with cloudflare: " + cf_response["errors"][0]["message"]}),
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
## ALL OK - create an invoice and return it to the user
|
||||||
|
sats = g.data["sats"]
|
||||||
|
payment_hash, payment_request = await create_invoice(
|
||||||
|
wallet_id=domain.wallet,
|
||||||
|
amount=sats,
|
||||||
|
memo=f"subdomain {g.data['subdomain']}.{domain.domain} for {sats} sats for {g.data['duration']} days",
|
||||||
|
extra={"tag": "lnsubdomain"},
|
||||||
|
)
|
||||||
|
|
||||||
|
subdomain = await create_subdomain(payment_hash=payment_hash, wallet=domain.wallet, **g.data)
|
||||||
|
|
||||||
|
if not subdomain:
|
||||||
|
return jsonify({"message": "LNsubdomain could not be fetched."}), HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/api/v1/subdomains/<payment_hash>", methods=["GET"])
|
||||||
|
async def api_subdomain_send_subdomain(payment_hash):
|
||||||
|
subdomain = await get_subdomain(payment_hash)
|
||||||
|
try:
|
||||||
|
status = await check_invoice_status(subdomain.wallet, payment_hash)
|
||||||
|
is_paid = not status.pending
|
||||||
|
except Exception:
|
||||||
|
return jsonify({"paid": False}), HTTPStatus.OK
|
||||||
|
|
||||||
|
if is_paid:
|
||||||
|
return jsonify({"paid": True}), HTTPStatus.OK
|
||||||
|
|
||||||
|
return jsonify({"paid": False}), HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
@subdomains_ext.route("/api/v1/subdomains/<subdomain_id>", methods=["DELETE"])
|
||||||
|
@api_check_wallet_key("invoice")
|
||||||
|
async def api_subdomain_delete(subdomain_id):
|
||||||
|
subdomain = await get_subdomain(subdomain_id)
|
||||||
|
|
||||||
|
if not subdomain:
|
||||||
|
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
if subdomain.wallet != g.wallet.id:
|
||||||
|
return jsonify({"message": "Not your subdomain."}), HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
|
await delete_subdomain(subdomain_id)
|
||||||
|
|
||||||
|
return "", HTTPStatus.NO_CONTENT
|
|
@ -17,7 +17,7 @@
|
||||||
<code>[<tpos_object>, ...]</code>
|
<code>[<tpos_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root }}tpos/api/v1/tposs -H "X-Api-Key:
|
>curl -X GET {{ request.url_root }}api/v1/tposs -H "X-Api-Key:
|
||||||
<invoice_key>"
|
<invoice_key>"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}tpos/api/v1/tposs -d '{"name":
|
>curl -X POST {{ request.url_root }}api/v1/tposs -d '{"name":
|
||||||
<string>, "currency": <string>}' -H "Content-type:
|
<string>, "currency": <string>}' -H "Content-type:
|
||||||
application/json" -H "X-Api-Key: <admin_key>"
|
application/json" -H "X-Api-Key: <admin_key>"
|
||||||
</code>
|
</code>
|
||||||
|
@ -69,8 +69,8 @@
|
||||||
<code></code>
|
<code></code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.url_root
|
>curl -X DELETE {{ request.url_root }}api/v1/tposs/<tpos_id> -H
|
||||||
}}tpos/api/v1/tposs/<tpos_id> -H "X-Api-Key: <admin_key>"
|
"X-Api-Key: <admin_key>"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
@ -13,9 +13,8 @@ from .crud import create_tpos, get_tpos, get_tposs, delete_tpos
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_tposs():
|
async def api_tposs():
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if "all_wallets" in request.args:
|
if "all_wallets" in request.args:
|
||||||
wallet_ids = await get_user(g.wallet.user).wallet_ids
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
|
|
||||||
return jsonify([tpos._asdict() for tpos in await get_tposs(wallet_ids)]), HTTPStatus.OK
|
return jsonify([tpos._asdict() for tpos in await get_tposs(wallet_ids)]), HTTPStatus.OK
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
<code>JSON list of users</code>
|
<code>JSON list of users</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root }}usermanager/api/v1/users -H
|
>curl -X GET {{ request.url_root }}api/v1/users -H
|
||||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root
|
>curl -X GET {{ request.url_root
|
||||||
}}usermanager/api/v1/wallets/<user_id> -H "X-Api-Key: {{
|
}}api/v1/wallets/<user_id> -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}"
|
g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root
|
>curl -X GET {{ request.url_root
|
||||||
}}usermanager/api/v1/wallets<wallet_id> -H "X-Api-Key: {{
|
}}api/v1/wallets<wallet_id> -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}"
|
g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/users -d
|
>curl -X POST {{ request.url_root }}api/v1/users -d
|
||||||
'{"admin_id": "{{ g.user.id }}", "wallet_name": <string>,
|
'{"admin_id": "{{ g.user.id }}", "wallet_name": <string>,
|
||||||
"user_name": <string>}' -H "X-Api-Key: {{
|
"user_name": <string>}' -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
|
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
|
||||||
|
@ -165,7 +165,7 @@
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d
|
>curl -X POST {{ request.url_root }}api/v1/wallets -d
|
||||||
'{"user_id": <string>, "wallet_name": <string>,
|
'{"user_id": <string>, "wallet_name": <string>,
|
||||||
"admin_id": "{{ g.user.id }}"}' -H "X-Api-Key: {{
|
"admin_id": "{{ g.user.id }}"}' -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
|
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
|
||||||
|
@ -190,7 +190,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.url_root
|
>curl -X DELETE {{ request.url_root
|
||||||
}}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{
|
}}api/v1/users/<user_id> -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}"
|
g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -208,7 +208,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.url_root
|
>curl -X DELETE {{ request.url_root
|
||||||
}}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{
|
}}api/v1/wallets/<wallet_id> -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}"
|
g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -230,7 +230,7 @@
|
||||||
<code>{"X-Api-Key": <string>}</code>
|
<code>{"X-Api-Key": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d
|
>curl -X POST {{ request.url_root }}api/v1/extensions -d
|
||||||
'{"userid": <string>, "extension": <string>, "active":
|
'{"userid": <string>, "extension": <string>, "active":
|
||||||
<integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
<integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
||||||
"Content-type: application/json"
|
"Content-type: application/json"
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<code>[<withdraw_link_object>, ...]</code>
|
<code>[<withdraw_link_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root }}withdraw/api/v1/links -H
|
>curl -X GET {{ request.url_root }}api/v1/links -H
|
||||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.url_root
|
>curl -X GET {{ request.url_root
|
||||||
}}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{
|
}}api/v1/links/<withdraw_id> -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].inkey }}"
|
g.user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
<code>{"lnurl": <string>}</code>
|
<code>{"lnurl": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}withdraw/api/v1/links -d
|
>curl -X POST {{ request.url_root }}api/v1/links -d
|
||||||
'{"title": <string>, "min_withdrawable": <integer>,
|
'{"title": <string>, "min_withdrawable": <integer>,
|
||||||
"max_withdrawable": <integer>, "uses": <integer>,
|
"max_withdrawable": <integer>, "uses": <integer>,
|
||||||
"wait_time": <integer>, "is_unique": <boolean>}' -H
|
"wait_time": <integer>, "is_unique": <boolean>}' -H
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X PUT {{ request.url_root
|
>curl -X PUT {{ request.url_root
|
||||||
}}withdraw/api/v1/links/<withdraw_id> -d '{"title":
|
}}api/v1/links/<withdraw_id> -d '{"title":
|
||||||
<string>, "min_withdrawable": <integer>,
|
<string>, "min_withdrawable": <integer>,
|
||||||
"max_withdrawable": <integer>, "uses": <integer>,
|
"max_withdrawable": <integer>, "uses": <integer>,
|
||||||
"wait_time": <integer>, "is_unique": <boolean>}' -H
|
"wait_time": <integer>, "is_unique": <boolean>}' -H
|
||||||
|
@ -146,7 +146,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.url_root
|
>curl -X DELETE {{ request.url_root
|
||||||
}}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{
|
}}api/v1/links/<withdraw_id> -H "X-Api-Key: {{
|
||||||
g.user.wallets[0].adminkey }}"
|
g.user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
@ -140,7 +140,10 @@ window.LNbits = {
|
||||||
'bolt11',
|
'bolt11',
|
||||||
'preimage',
|
'preimage',
|
||||||
'payment_hash',
|
'payment_hash',
|
||||||
'extra'
|
'extra',
|
||||||
|
'wallet_id',
|
||||||
|
'webhook',
|
||||||
|
'webhook_status'
|
||||||
],
|
],
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
|
|
|
@ -204,6 +204,15 @@ Vue.component('lnbits-payment-details', {
|
||||||
<div class="col-3"><b>Payment hash</b>:</div>
|
<div class="col-3"><b>Payment hash</b>:</div>
|
||||||
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
|
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" v-if="payment.webhook">
|
||||||
|
<div class="col-3"><b>Webhook</b>:</div>
|
||||||
|
<div class="col-9 text-wrap mono">
|
||||||
|
{{ payment.webhook }}
|
||||||
|
<q-badge :color="webhookStatusColor" text-color="white">
|
||||||
|
{{ webhookStatusText }}
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row" v-if="hasPreimage">
|
<div class="row" v-if="hasPreimage">
|
||||||
<div class="col-3"><b>Payment proof</b>:</div>
|
<div class="col-3"><b>Payment proof</b>:</div>
|
||||||
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
|
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
|
||||||
|
@ -243,6 +252,19 @@ Vue.component('lnbits-payment-details', {
|
||||||
this.payment.extra.success_action
|
this.payment.extra.success_action
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
webhookStatusColor() {
|
||||||
|
return this.payment.webhook_status >= 300 ||
|
||||||
|
this.payment.webhook_status < 0
|
||||||
|
? 'red-10'
|
||||||
|
: !this.payment.webhook_status
|
||||||
|
? 'cyan-7'
|
||||||
|
: 'green-10'
|
||||||
|
},
|
||||||
|
webhookStatusText() {
|
||||||
|
return this.payment.webhook_status
|
||||||
|
? this.payment.webhook_status
|
||||||
|
: 'not sent yet'
|
||||||
|
},
|
||||||
hasTag() {
|
hasTag() {
|
||||||
return this.payment.extra && !!this.payment.extra.tag
|
return this.payment.extra && !!this.payment.extra.tag
|
||||||
},
|
},
|
||||||
|
|
|
@ -82,7 +82,7 @@ class LntxbotWallet(Wallet):
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
fee_msat = data["fee_msat"]
|
fee_msat = -data["fee_msat"]
|
||||||
preimage = data["payment_preimage"]
|
preimage = data["payment_preimage"]
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue