From 3c398a82760a78d05674dd9d5b0ada6b0c4828b2 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 19:51:45 +0100 Subject: [PATCH 01/25] started working on subdomains extension --- lnbits/extensions/subdomains/README.md | 65 ++++ lnbits/extensions/subdomains/__init__.py | 10 + lnbits/extensions/subdomains/config.json | 6 + lnbits/extensions/subdomains/migrations.py | 34 ++ lnbits/extensions/subdomains/models.py | 26 ++ .../templates/subdomains/index.html | 296 ++++++++++++++++++ lnbits/extensions/subdomains/views.py | 12 + lnbits/extensions/subdomains/views_api.py | 40 +++ 8 files changed, 489 insertions(+) create mode 100644 lnbits/extensions/subdomains/README.md create mode 100644 lnbits/extensions/subdomains/__init__.py create mode 100644 lnbits/extensions/subdomains/config.json create mode 100644 lnbits/extensions/subdomains/migrations.py create mode 100644 lnbits/extensions/subdomains/models.py create mode 100644 lnbits/extensions/subdomains/templates/subdomains/index.html create mode 100644 lnbits/extensions/subdomains/views.py create mode 100644 lnbits/extensions/subdomains/views_api.py diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md new file mode 100644 index 000000000..ca3fce5ae --- /dev/null +++ b/lnbits/extensions/subdomains/README.md @@ -0,0 +1,65 @@ +

Subdomains Extension

+ +#TODO - fix formatting etc... +on lnbits there should be an interface with input fields: +subdomain (for example: subdomain1) +ip address (for example: 192.168.21.21) +duration (1 month / 1 year etc...) + +then when user presses SUBMIT button the ln invoice is shown that has to be paid... + +when invoice is paid, the lnbits backend send request to the cloudflare domain registration service, that creates a new A record for that subdomain + +for example, i am hosting lnbits on +lnbits.grmkris.com + +and i am selling my subdomains +subdomain1.grmkris.com +subdomain2.grmkris.com +subdomain3.grmkris.com + +there should be checks if that subdomain is already taken + +and maybe an option to blacklist certain subdomains that i don't want to sell + + +

If your extension has API endpoints, include useful ones here

+ +curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"subdomains"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" + +## 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 + + + +example curls: +List dns records +```bash +curl --location --request GET 'https://api.cloudflare.com/client/v4/zones/bf3c1e516b35878c9f6532db2f2705ee/dns_records?type=A' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer mS3gGFC3ySLqBe2ERtRTlh7H2YiGbFp2KLDK62uu' +``` + +```bash +curl --location --request POST 'https://api.cloudflare.com/client/v4/zones/bf3c1e516b35878c9f6532db2f2705ee/dns_records' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer mS3gGFC3ySLqBe2ERtRTlh7H2YiGbFp2KLDK62uu' \ +--data-raw '{ + "type":"A", + "name":"subdomain1.grmkris.com", + "content":"31.15.150.237", + "ttl":0, + "proxied":true +}' +``` \ No newline at end of file diff --git a/lnbits/extensions/subdomains/__init__.py b/lnbits/extensions/subdomains/__init__.py new file mode 100644 index 000000000..51a821174 --- /dev/null +++ b/lnbits/extensions/subdomains/__init__.py @@ -0,0 +1,10 @@ +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 diff --git a/lnbits/extensions/subdomains/config.json b/lnbits/extensions/subdomains/config.json new file mode 100644 index 000000000..4a34be565 --- /dev/null +++ b/lnbits/extensions/subdomains/config.json @@ -0,0 +1,6 @@ +{ + "name": "Subdomains", + "short_description": "Sell subdomains of your domain", + "icon": "domain", + "contributors": ["grmkris"] +} diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py new file mode 100644 index 000000000..00010f60f --- /dev/null +++ b/lnbits/extensions/subdomains/migrations.py @@ -0,0 +1,34 @@ +async def m001_initial(db): + + await db.execute( + """ + CREATE TABLE IF NOT EXISTS domain ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + domain_name 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, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + ); + """ + ) + + await db.execute( + """ + CREATE TABLE IF NOT EXISTS subdomain ( + id TEXT PRIMARY KEY, + domain_name TEXT NOT NULL, + email TEXT NOT NULL, + subdomain TEXT NOT NULL, + ip TEXT NOT NULL, + wallet TEXT NOT NULL, + sats INTEGER NOT NULL, + paid BOOLEAN NOT NULL, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + ); + """ + ) \ No newline at end of file diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py new file mode 100644 index 000000000..9b330b858 --- /dev/null +++ b/lnbits/extensions/subdomains/models.py @@ -0,0 +1,26 @@ +from typing import NamedTuple + + +class Domains(NamedTuple): + id: str + wallet: str + domainName: str + cfToken: str + cfZoneId: str + webhook: str + description: str + cost: int + amountmade: int + time: int + + +class Subdomains(NamedTuple): + id: str + domainName: str + email: str + subdomain: str + ip: str + wallet: str + sats: int + paid: bool + time: int diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html new file mode 100644 index 000000000..cc6a37362 --- /dev/null +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -0,0 +1,296 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + +
+
+ + + New Domain + + + + + +
+
+
Domains
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ + + + + + + + + + + + + + +
+ Update Form + Create Domain + Cancel +
+
+
+
+ +
+ +{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} \ No newline at end of file diff --git a/lnbits/extensions/subdomains/views.py b/lnbits/extensions/subdomains/views.py new file mode 100644 index 000000000..b75c4906b --- /dev/null +++ b/lnbits/extensions/subdomains/views.py @@ -0,0 +1,12 @@ +from quart import g, render_template + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import subdomains_ext + + +@subdomains_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("subdomains/index.html", user=g.user) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py new file mode 100644 index 000000000..bfcac16c7 --- /dev/null +++ b/lnbits/extensions/subdomains/views_api.py @@ -0,0 +1,40 @@ +# views_api.py is for you API endpoints that could be hit by another service + +# add your dependencies here + +# import json +# import httpx +# (use httpx just like requests, except instead of response.ok there's only the +# response.is_error that is its inverse) + +from quart import jsonify +from http import HTTPStatus + +from . import subdomains_ext + + +# add your endpoints here + + +@subdomains_ext.route("/api/v1/tools", methods=["GET"]) +async def api_subdomains(): + """Try to add descriptions for others.""" + tools = [ + { + "name": "Quart", + "url": "https://pgjones.gitlab.io/quart/", + "language": "Python", + }, + { + "name": "Vue.js", + "url": "https://vuejs.org/", + "language": "JavaScript", + }, + { + "name": "Quasar Framework", + "url": "https://quasar.dev/", + "language": "JavaScript", + }, + ] + + return jsonify(tools), HTTPStatus.OK From 307a919d179a0b456afbcc0db9d0793eab6dfa76 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 22:32:04 +0100 Subject: [PATCH 02/25] added CRUD operations --- lnbits/extensions/subdomains/crud.py | 140 ++++++++++++++++++ lnbits/extensions/subdomains/migrations.py | 4 +- lnbits/extensions/subdomains/models.py | 10 +- .../templates/subdomains/index.html | 8 +- lnbits/extensions/subdomains/views_api.py | 1 + 5 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 lnbits/extensions/subdomains/crud.py diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py new file mode 100644 index 000000000..217d6f025 --- /dev/null +++ b/lnbits/extensions/subdomains/crud.py @@ -0,0 +1,140 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import Domains, Subdomains +import httpx + +from lnbits.extensions import subdomains + +async def create_subdomain( + payment_hash: str, + wallet: str, + domain: str, + subdomain: str, + email: str, + ip: str, + sats: int, +) -> Subdomains: + await db.execute( + """ + INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, paid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + (payment_hash, domain, email, subdomain, ip, wallet, sats, False), + ) + + 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 * FROM subdomain WHERE id = ?", (payment_hash,)) + if row[7] == 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[7] + await db.execute( + """ + UPDATE domain + SET amountmade = ? + WHERE id = ? + """, + (amount, row[1]), + ) + + subdomain = await get_subdomain(payment_hash) + if domaindata.webhook: + async with httpx.AsyncClient() as client: + try: + r = await client.post( + domaindata.webhook, + json={ + "domain": subdomain.domain, + "subdomain": subdomain.subdomain, + "email": subdomain.email, + "ip": subdomain.ip + }, + timeout=40, + ) + except AssertionError: + webhook = None + return subdomain + subdomain = await get_subdomain(payment_hash) + return + + +async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]: + row = await db.fetchone("SELECT * FROM subdomain WHERE id = ?", (subdomain_id,)) + 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 * FROM subdomain WHERE 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, cfToken: str, cfZoneId: str, webhook: Optional[str] = None, description: str, cost: int) -> Domains: + domain_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO domains (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade) + VALUES (?, ?, ?, ?, ?, ?, ?. ?, ?) + """, + (domain_id, wallet, domain, webhook, cfToken, cfZoneId, description, cost, 0), + ) + + 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,)) diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py index 00010f60f..7d4fa4d71 100644 --- a/lnbits/extensions/subdomains/migrations.py +++ b/lnbits/extensions/subdomains/migrations.py @@ -5,7 +5,7 @@ async def m001_initial(db): CREATE TABLE IF NOT EXISTS domain ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, - domain_name TEXT NOT NULL, + domain TEXT NOT NULL, webhook TEXT, cf_token TEXT NOT NULL, cf_zone_id TEXT NOT NULL, @@ -21,7 +21,7 @@ async def m001_initial(db): """ CREATE TABLE IF NOT EXISTS subdomain ( id TEXT PRIMARY KEY, - domain_name TEXT NOT NULL, + domain TEXT NOT NULL, email TEXT NOT NULL, subdomain TEXT NOT NULL, ip TEXT NOT NULL, diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py index 9b330b858..0a71a1dd8 100644 --- a/lnbits/extensions/subdomains/models.py +++ b/lnbits/extensions/subdomains/models.py @@ -4,7 +4,7 @@ from typing import NamedTuple class Domains(NamedTuple): id: str wallet: str - domainName: str + domain: str cfToken: str cfZoneId: str webhook: str @@ -16,11 +16,11 @@ class Domains(NamedTuple): class Subdomains(NamedTuple): id: str - domainName: str - email: str - subdomain: str - ip: str wallet: str + domain: str + subdomain: str + email: str + ip: str sats: int paid: bool time: int diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index cc6a37362..183fc0221 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -207,18 +207,18 @@ createDomain: function (wallet, data) { var self = this - /* + LNbits.api - .request('POST', '/lnticket/api/v1/forms', wallet.inkey, data) + .request('POST', '/subdomains/api/v1/domains', wallet.inkey, data) .then(function (response) { - self.forms.push(mapLNTicket(response.data)) + self.forms.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.forms, {id: formId}) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py index bfcac16c7..dc9263ca2 100644 --- a/lnbits/extensions/subdomains/views_api.py +++ b/lnbits/extensions/subdomains/views_api.py @@ -38,3 +38,4 @@ async def api_subdomains(): ] return jsonify(tools), HTTPStatus.OK + From 2eb44674d0bb66bdf333a021b07c91281e630d16 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 22:40:46 +0100 Subject: [PATCH 03/25] added public facing api --- lnbits/extensions/subdomains/views_api.py | 185 ++++++++++++++++++---- 1 file changed, 153 insertions(+), 32 deletions(-) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py index dc9263ca2..7f2ffb7b4 100644 --- a/lnbits/extensions/subdomains/views_api.py +++ b/lnbits/extensions/subdomains/views_api.py @@ -1,41 +1,162 @@ -# views_api.py is for you API endpoints that could be hit by another service - -# add your dependencies here - -# import json -# import httpx -# (use httpx just like requests, except instead of response.ok there's only the -# response.is_error that is its inverse) - -from quart import jsonify +import re +from quart import g, jsonify, request from http import HTTPStatus -from . import subdomains_ext +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 . import lnsubdomain_ext +from .crud import ( + create_subdomain, + set_subdomain_paid, + get_subdomain, + get_subdomains, + delete_subdomain, + create_domain, + update_domain, + get_domain, + get_domains, + delete_domain, +) -# add your endpoints here +# domainS -@subdomains_ext.route("/api/v1/tools", methods=["GET"]) +@lnsubdomain_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 + + +@lnsubdomain_ext.route("/api/v1/domains", methods=["POST"]) +@lnsubdomain_ext.route("/api/v1/domains/", 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}, + "cfToken": {"type": "string", "empty": False, "required": True}, + "cfZoneId": {"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}, + } +) +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 + + +@lnsubdomain_ext.route("/api/v1/domains/", 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########## + + +@lnsubdomain_ext.route("/api/v1/subdomains", methods=["GET"]) +@api_check_wallet_key("invoice") async def api_subdomains(): - """Try to add descriptions for others.""" - tools = [ - { - "name": "Quart", - "url": "https://pgjones.gitlab.io/quart/", - "language": "Python", - }, - { - "name": "Vue.js", - "url": "https://vuejs.org/", - "language": "JavaScript", - }, - { - "name": "Quasar Framework", - "url": "https://quasar.dev/", - "language": "JavaScript", - }, - ] + wallet_ids = [g.wallet.id] - return jsonify(tools), HTTPStatus.OK + 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 + + +@lnsubdomain_ext.route("/api/v1/subdomains/", 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}, + } +) +async def api_subdomain_make_subdomain(domain_id): + domain = await get_domain(domain_id) + if not domain: + return jsonify({"message": "LNsubdomain does not exist."}), HTTPStatus.NOT_FOUND + + subdomain = len(re.split(r"\s+", g.data["subdomain"])) + sats = g.data["sats"] + payment_hash, payment_request = await create_invoice( + wallet_id=domain.wallet, + amount=sats, + memo=f"subdomain with {subdomain} words on {domain_id}", + 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 + + +@lnsubdomain_ext.route("/api/v1/subdomains/", 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: + wallet = await get_wallet(subdomain.wallet) + payment = await wallet.get_payment(payment_hash) + await payment.set_pending(False) + subdomain = await set_subdomain_paid(payment_hash=payment_hash) + return jsonify({"paid": True}), HTTPStatus.OK + + return jsonify({"paid": False}), HTTPStatus.OK + + +@lnsubdomain_ext.route("/api/v1/subdomains/", 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 From e96ec08f44e4b36299509d8f075ecdd98dbd57ee Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 23:24:47 +0100 Subject: [PATCH 04/25] code refactor --- lnbits/extensions/subdomains/crud.py | 4 +- lnbits/extensions/subdomains/models.py | 4 +- .../templates/subdomains/index.html | 71 +++++++++---------- lnbits/extensions/subdomains/views_api.py | 18 ++--- 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py index 217d6f025..ca46d2fba 100644 --- a/lnbits/extensions/subdomains/crud.py +++ b/lnbits/extensions/subdomains/crud.py @@ -102,8 +102,8 @@ async def create_domain(*, wallet: str, domain: str, cfToken: str, cfZoneId: str domain_id = urlsafe_short_hash() await db.execute( """ - INSERT INTO domains (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade) - VALUES (?, ?, ?, ?, ?, ?, ?. ?, ?) + INSERT INTO domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, (domain_id, wallet, domain, webhook, cfToken, cfZoneId, description, cost, 0), ) diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py index 0a71a1dd8..e248811d9 100644 --- a/lnbits/extensions/subdomains/models.py +++ b/lnbits/extensions/subdomains/models.py @@ -5,8 +5,8 @@ class Domains(NamedTuple): id: str wallet: str domain: str - cfToken: str - cfZoneId: str + cf_token: str + cf_zone_id: str webhook: str description: str cost: int diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index 183fc0221..e2d08a63e 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -61,7 +61,7 @@ - + @@ -73,7 +73,7 @@
Update Form Create Domain Cancel
@@ -104,7 +104,7 @@ domainsTable: { columns: [ {name: 'id', align: 'left', label: 'ID', field: 'id'}, - {name: 'domainName', align: 'left', label: 'Domain name', field: 'name'}, + {name: 'domain', align: 'left', label: 'Domain name', field: 'domain'}, {name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'}, {name: 'webhook', align: 'left', label: 'Webhook', field: 'webhook'}, { @@ -133,33 +133,32 @@ methods: { getSubdomains: function () { var self = this - /* + LNbits.api .request( 'GET', - '/lnticket/api/v1/tickets?all_wallets', + '/subdomains/api/v1/subdomains?all_wallets', this.g.user.wallets[0].inkey ) .then(function (response) { self.tickets = response.data.map(function (obj) { - return mapLNTicket(obj) + return mapLNSubdomain(obj) }) }) - */ }, deleteSubdomain: function (subdomainId) { var self = this var tickets = _.findWhere(this.tickets, {id: ticketId}) - /* + LNbits.utils .confirmDialog('Are you sure you want to delete this ticket') .onOk(function () { LNbits.api .request( 'DELETE', - '/lnticket/api/v1/tickets/' + ticketId, - _.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey + '/subdomain/api/v1/subdomains/' + subdomainId, + _.findWhere(self.g.user.wallets, {id: subdomains.wallet}).inkey ) .then(function (response) { self.tickets = _.reject(self.tickets, function (obj) { @@ -170,7 +169,7 @@ LNbits.utils.notifyApiError(error) }) }) - */ + }, exportSubdomainsCSV: function () { LNbits.utils.exportCSV(this.domainsTable.columns, this.tickets) @@ -178,19 +177,19 @@ getDomains: function () { var self = this - /* + LNbits.api .request( 'GET', - '/lnticket/api/v1/forms?all_wallets', + '/subdomains/api/v1/domains?all_wallets', this.g.user.wallets[0].inkey ) .then(function (response) { - self.forms = response.data.map(function (obj) { - return mapLNTicket(obj) + self.domains = response.data.map(function (obj) { + return mapLNDomain(obj) }) }) - */ + }, sendFormData: function () { var wallet = _.findWhere(this.g.user.wallets, { @@ -199,9 +198,9 @@ var data = this.domainDialog.data if (data.id) { - this.updateForm(wallet, data) + this.updateDomain(wallet, data) } else { - this.createForm(wallet, data) + this.createDomain(wallet, data) } }, @@ -211,7 +210,7 @@ LNbits.api .request('POST', '/subdomains/api/v1/domains', wallet.inkey, data) .then(function (response) { - self.forms.push(mapLNDomain(response.data)) + self.domains.push(mapLNDomain(response.data)) self.domainDialog.show = false self.domainDialog.data = {} }) @@ -221,13 +220,13 @@ }, updateDomainDialog: function (formId) { - var link = _.findWhere(this.forms, {id: formId}) + var link = _.findWhere(this.domains, {id: formId}) console.log(link.id) this.domainDialog.data.id = link.id this.domainDialog.data.wallet = link.wallet - this.domainDialog.data.domainName = link.domainName + this.domainDialog.data.domain = link.domain this.domainDialog.data.description = link.description - this.domainDialog.domainDialog.data.cfToken = link.cfToken + this.domainDialog.data.cfToken = link.cfToken this.domainDialog.cfZoneId = link.cfZoneId this.domainDialog.data.cost = link.cost this.domainDialog.show = true @@ -235,51 +234,51 @@ updateDomain: function (wallet, data) { var self = this console.log(data) - /* + LNbits.api .request( 'PUT', - '/lnticket/api/v1/forms/' + data.id, + '/subdomains/api/v1/domains/' + data.id, wallet.inkey, data ) .then(function (response) { - self.forms = _.reject(self.forms, function (obj) { + self.domains = _.reject(self.domains, function (obj) { return obj.id == data.id }) - self.forms.push(mapLNTicket(response.data)) + self.domains.push(mapLNDomain(response.data)) self.domainDialog.show = false self.domainDialog.data = {} }) .catch(function (error) { LNbits.utils.notifyApiError(error) }) - */ + }, - deleteDomain: function (formsId) { + deleteDomain: function (domainId) { var self = this - var forms = _.findWhere(this.forms, {id: formsId}) - /* + var domains = _.findWhere(this.domains, {id: domainId}) + LNbits.utils - .confirmDialog('Are you sure you want to delete this form link?') + .confirmDialog('Are you sure you want to delete this domain link?') .onOk(function () { LNbits.api .request( 'DELETE', - '/lnticket/api/v1/forms/' + formsId, - _.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey + '/subdomains/api/v1/domains/' + domainId, + _.findWhere(self.g.user.wallets, {id: domains.wallet}).inkey ) .then(function (response) { - self.forms = _.reject(self.forms, function (obj) { - return obj.id == formsId + 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) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py index 7f2ffb7b4..a05c32c22 100644 --- a/lnbits/extensions/subdomains/views_api.py +++ b/lnbits/extensions/subdomains/views_api.py @@ -6,7 +6,7 @@ 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 . import lnsubdomain_ext +from . import subdomains_ext from .crud import ( create_subdomain, set_subdomain_paid, @@ -24,7 +24,7 @@ from .crud import ( # domainS -@lnsubdomain_ext.route("/api/v1/domains", methods=["GET"]) +@subdomains_ext.route("/api/v1/domains", methods=["GET"]) @api_check_wallet_key("invoice") async def api_domains(): wallet_ids = [g.wallet.id] @@ -35,8 +35,8 @@ async def api_domains(): return jsonify([domain._asdict() for domain in await get_domains(wallet_ids)]), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/domains", methods=["POST"]) -@lnsubdomain_ext.route("/api/v1/domains/", methods=["PUT"]) +@subdomains_ext.route("/api/v1/domains", methods=["POST"]) +@subdomains_ext.route("/api/v1/domains/", methods=["PUT"]) @api_check_wallet_key("invoice") @api_validate_post_request( schema={ @@ -65,7 +65,7 @@ async def api_domain_create(domain_id=None): return jsonify(domain._asdict()), HTTPStatus.CREATED -@lnsubdomain_ext.route("/api/v1/domains/", methods=["DELETE"]) +@subdomains_ext.route("/api/v1/domains/", methods=["DELETE"]) @api_check_wallet_key("invoice") async def api_domain_delete(domain_id): domain = await get_domain(domain_id) @@ -84,7 +84,7 @@ async def api_domain_delete(domain_id): #########subdomains########## -@lnsubdomain_ext.route("/api/v1/subdomains", methods=["GET"]) +@subdomains_ext.route("/api/v1/subdomains", methods=["GET"]) @api_check_wallet_key("invoice") async def api_subdomains(): wallet_ids = [g.wallet.id] @@ -95,7 +95,7 @@ async def api_subdomains(): return jsonify([domain._asdict() for domain in await get_subdomains(wallet_ids)]), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["POST"]) +@subdomains_ext.route("/api/v1/subdomains/", methods=["POST"]) @api_validate_post_request( schema={ "domain": {"type": "string", "empty": False, "required": True}, @@ -127,7 +127,7 @@ async def api_subdomain_make_subdomain(domain_id): return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["GET"]) +@subdomains_ext.route("/api/v1/subdomains/", methods=["GET"]) async def api_subdomain_send_subdomain(payment_hash): subdomain = await get_subdomain(payment_hash) try: @@ -146,7 +146,7 @@ async def api_subdomain_send_subdomain(payment_hash): return jsonify({"paid": False}), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["DELETE"]) +@subdomains_ext.route("/api/v1/subdomains/", methods=["DELETE"]) @api_check_wallet_key("invoice") async def api_subdomain_delete(subdomain_id): subdomain = await get_subdomain(subdomain_id) From c0d73711370b8decb9d9fb27f9e81cbaf601b147 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Tue, 29 Dec 2020 19:16:04 +0100 Subject: [PATCH 05/25] subdomains public window --- lnbits/extensions/subdomains/crud.py | 3 + .../templates/subdomains/display.html | 168 ++++++++++++++++++ .../templates/subdomains/index.html | 5 +- lnbits/extensions/subdomains/views.py | 19 +- 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 lnbits/extensions/subdomains/templates/subdomains/display.html diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py index ca46d2fba..4bad6768f 100644 --- a/lnbits/extensions/subdomains/crud.py +++ b/lnbits/extensions/subdomains/crud.py @@ -138,3 +138,6 @@ async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]: async def delete_domain(domain_id: str) -> None: await db.execute("DELETE FROM domain WHERE id = ?", (domain_id,)) + + + diff --git a/lnbits/extensions/subdomains/templates/subdomains/display.html b/lnbits/extensions/subdomains/templates/subdomains/display.html new file mode 100644 index 000000000..f57e079f9 --- /dev/null +++ b/lnbits/extensions/subdomains/templates/subdomains/display.html @@ -0,0 +1,168 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +

{{ form_domain }}

+
+
{{ form_desc }}
+
+ + + + + + + + +

{% raw %}{{amountSats}}{% endraw %}

+
+ Submit + Cancel +
+
+
+
+
+ + + + + + +
+ Copy invoice + Close +
+
+
+
+ +{% endblock %} {% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index e2d08a63e..7428023b4 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -226,8 +226,9 @@ this.domainDialog.data.wallet = link.wallet this.domainDialog.data.domain = link.domain this.domainDialog.data.description = link.description - this.domainDialog.data.cfToken = link.cfToken - this.domainDialog.cfZoneId = link.cfZoneId + this.domainDialog.data.cfToken = link.cf_token + this.domainDialog.data.cfZoneId = link.cf_zone_id + this.domainDialog.data.webhook = link.webhook this.domainDialog.data.cost = link.cost this.domainDialog.show = true }, diff --git a/lnbits/extensions/subdomains/views.py b/lnbits/extensions/subdomains/views.py index b75c4906b..c90c4dbc8 100644 --- a/lnbits/extensions/subdomains/views.py +++ b/lnbits/extensions/subdomains/views.py @@ -1,12 +1,27 @@ -from quart import g, render_template +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("/") +async def display(domain_id): + domain = await get_domain(domain_id) + if not domain: + abort(HTTPStatus.NOT_FOUND, "Domain does not exist.") + + return await render_template( + "subdomains/display.html", + domain_id=domain.id, + domain_domain=domain.domain, + form_desc=domain.description, + form_cost=domain.cost, + ) From 6c4b5ea406ac2f8c19dcdda6d9356945f0c884aa Mon Sep 17 00:00:00 2001 From: Kristjan Date: Tue, 29 Dec 2020 20:52:54 +0100 Subject: [PATCH 06/25] working subdomains frontend, table, popup, payments --- lnbits/extensions/subdomains/crud.py | 15 +- lnbits/extensions/subdomains/migrations.py | 1 + lnbits/extensions/subdomains/models.py | 2 + .../templates/subdomains/display.html | 27 ++-- .../templates/subdomains/index.html | 140 ++++++++++++++---- lnbits/extensions/subdomains/views.py | 4 +- lnbits/extensions/subdomains/views_api.py | 6 +- 7 files changed, 143 insertions(+), 52 deletions(-) diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py index 4bad6768f..0ecb88147 100644 --- a/lnbits/extensions/subdomains/crud.py +++ b/lnbits/extensions/subdomains/crud.py @@ -16,13 +16,14 @@ async def create_subdomain( email: str, ip: str, sats: int, + duration: int ) -> Subdomains: await db.execute( """ - INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, paid) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?,?) """, - (payment_hash, domain, email, subdomain, ip, wallet, sats, False), + (payment_hash, domain, email, subdomain, ip, wallet, sats, duration, False), ) subdomain = await get_subdomain(payment_hash) @@ -32,7 +33,7 @@ async def create_subdomain( async def set_subdomain_paid(payment_hash: str) -> Subdomains: row = await db.fetchone("SELECT * FROM subdomain WHERE id = ?", (payment_hash,)) - if row[7] == False: + if row[8] == False: await db.execute( """ UPDATE subdomain @@ -45,7 +46,7 @@ async def set_subdomain_paid(payment_hash: str) -> Subdomains: domaindata = await get_domain(row[1]) assert domaindata, "Couldn't get domain from paid subdomain" - amount = domaindata.amountmade + row[7] + amount = domaindata.amountmade + row[8] await db.execute( """ UPDATE domain @@ -77,7 +78,7 @@ async def set_subdomain_paid(payment_hash: str) -> Subdomains: async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]: - row = await db.fetchone("SELECT * FROM subdomain WHERE id = ?", (subdomain_id,)) + row = await db.fetchone("SELECT * FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?", (subdomain_id,)) return Subdomains(**row) if row else None @@ -86,7 +87,7 @@ async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]: wallet_ids = [wallet_ids] q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall(f"SELECT * FROM subdomain WHERE wallet IN ({q})", (*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] diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py index 7d4fa4d71..75080280f 100644 --- a/lnbits/extensions/subdomains/migrations.py +++ b/lnbits/extensions/subdomains/migrations.py @@ -27,6 +27,7 @@ async def m001_initial(db): ip TEXT NOT NULL, wallet TEXT NOT NULL, sats INTEGER NOT NULL, + duration INTEGER NOT NULL, paid BOOLEAN NOT NULL, time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) ); diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py index e248811d9..a1d14070b 100644 --- a/lnbits/extensions/subdomains/models.py +++ b/lnbits/extensions/subdomains/models.py @@ -18,9 +18,11 @@ 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 diff --git a/lnbits/extensions/subdomains/templates/subdomains/display.html b/lnbits/extensions/subdomains/templates/subdomains/display.html index f57e079f9..90c91b581 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/display.html +++ b/lnbits/extensions/subdomains/templates/subdomains/display.html @@ -3,9 +3,9 @@
-

{{ form_domain }}

+

{{ domain_domain }}


-
{{ form_desc }}
+
{{ domain_desc }}

- +

{% raw %}{{amountSats}}{% endraw %}

@@ -48,7 +48,7 @@ {% endblock %} {% block scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index 8294f65f8..05168a798 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -5,7 +5,9 @@
- New Domain + New Domain @@ -16,11 +18,19 @@
Domains
- Export to CSV + Export to CSV
- + {% raw %}