mirror of https://github.com/lukechilds/lnbits.git
Arc
4 years ago
committed by
GitHub
27 changed files with 1522 additions and 36 deletions
@ -1,11 +1,11 @@ |
|||
#from sqlite3 import Row |
|||
#from typing import NamedTuple |
|||
# from sqlite3 import Row |
|||
# from typing import NamedTuple |
|||
|
|||
|
|||
#class Example(NamedTuple): |
|||
# class Example(NamedTuple): |
|||
# id: str |
|||
# wallet: str |
|||
# |
|||
# @classmethod |
|||
# def from_row(cls, row: Row) -> "Example": |
|||
# return cls(**dict(row)) |
|||
# return cls(**dict(row)) |
|||
|
@ -1,6 +1,6 @@ |
|||
{ |
|||
"name": "Support Tickets", |
|||
"short_description": "LN support ticket system", |
|||
"icon": "contact_support", |
|||
"contributors": ["benarc"] |
|||
"name": "Support Tickets", |
|||
"short_description": "LN support ticket system", |
|||
"icon": "contact_support", |
|||
"contributors": ["benarc"] |
|||
} |
|||
|
@ -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 |
@ -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)) |
@ -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" |
@ -0,0 +1,6 @@ |
|||
{ |
|||
"name": "Subdomains", |
|||
"short_description": "Sell subdomains of your domain", |
|||
"icon": "domain", |
|||
"contributors": ["grmkris"] |
|||
} |
@ -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,)) |
@ -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')) |
|||
); |
|||
""" |
|||
) |
@ -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 |
@ -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> |
@ -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 %} |
@ -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 %} |
@ -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 |
@ -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, |
|||
) |
@ -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 |
Loading…
Reference in new issue