remove subdomains

This commit is contained in:
dni ⚡ 2023-02-15 10:17:13 +01:00
parent 5bc2646103
commit e1d9bf31d1
No known key found for this signature in database
GPG key ID: 886317704CC4E618
15 changed files with 0 additions and 1541 deletions

View file

@ -1,54 +0,0 @@
<h1>Subdomains Extension</h1>
So the goal of the extension is to allow the owner of a domain to sell subdomains to anyone who is willing to pay some money for it.
[![video tutorial livestream](http://img.youtube.com/vi/O1X0fy3uNpw/0.jpg)](https://youtu.be/O1X0fy3uNpw 'video tutorial subdomains')
## 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 zone-ID 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
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 subdomain\
<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>
### 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

View file

@ -1,34 +0,0 @@
import asyncio
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_subdomains")
subdomains_ext: APIRouter = APIRouter(prefix="/subdomains", tags=["subdomains"])
subdomains_static_files = [
{
"path": "/subdomains/static",
"app": StaticFiles(directory="lnbits/extensions/subdomains/static"),
"name": "subdomains_static",
}
]
def subdomains_renderer():
return template_renderer(["lnbits/extensions/subdomains/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
def subdomains_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View file

@ -1,49 +0,0 @@
import httpx
from .models import Domains
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
async with httpx.AsyncClient() as client:
r = await client.post(
url,
headers=header,
json={
"type": record_type,
"name": aRecord,
"content": ip,
"ttl": 0,
"proxied": False,
},
timeout=40,
)
r.raise_for_status()
return r.json()
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:
await client.delete(url + "/" + domain_id, headers=header, timeout=40)

View file

@ -1,6 +0,0 @@
{
"name": "Subdomains",
"short_description": "Sell subdomains of your domain",
"tile": "/subdomains/static/image/subdomains.png",
"contributors": ["grmkris"]
}

View file

@ -1,161 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import CreateDomain, CreateSubdomain, Domains, Subdomains
async def create_subdomain(payment_hash, wallet, data: CreateSubdomain) -> Subdomains:
await db.execute(
"""
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
payment_hash,
data.domain,
data.email,
data.subdomain,
data.ip,
wallet,
data.sats,
data.duration,
False,
data.record_type,
),
)
new_subdomain = await get_subdomain(payment_hash)
assert new_subdomain, "Newly created subdomain couldn't be retrieved"
return new_subdomain
async def set_subdomain_paid(payment_hash: str) -> Subdomains:
row = await db.fetchone(
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
(payment_hash,),
)
if row[8] is False:
await db.execute(
"""
UPDATE subdomains.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 subdomains.domain
SET amountmade = ?
WHERE id = ?
""",
(amount, row[1]),
)
new_subdomain = await get_subdomain(payment_hash)
assert new_subdomain, "Newly paid subdomain couldn't be retrieved"
return new_subdomain
async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
row = await db.fetchone(
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
(subdomain_id,),
)
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 subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
(subdomain,),
)
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 subdomains.subdomain s INNER JOIN subdomains.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 subdomains.subdomain WHERE id = ?", (subdomain_id,))
# Domains
async def create_domain(data: CreateDomain) -> Domains:
domain_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO subdomains.domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
domain_id,
data.wallet,
data.domain,
data.webhook,
data.cf_token,
data.cf_zone_id,
data.description,
data.cost,
0,
data.allowed_record_types,
),
)
new_domain = await get_domain(domain_id)
assert new_domain, "Newly created domain couldn't be retrieved"
return new_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 subdomains.domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id)
)
row = await db.fetchone(
"SELECT * FROM subdomains.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 subdomains.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 subdomains.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 subdomains.domain WHERE id = ?", (domain_id,))

View file

@ -1,41 +0,0 @@
async def m001_initial(db):
await db.execute(
"""
CREATE TABLE subdomains.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 """
+ db.timestamp_now
+ """
);
"""
)
await db.execute(
"""
CREATE TABLE subdomains.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 """
+ db.timestamp_now
+ """
);
"""
)

View file

@ -1,52 +0,0 @@
from fastapi import Query
from pydantic import BaseModel
class CreateDomain(BaseModel):
wallet: str = Query(...)
domain: str = Query(...)
cf_token: str = Query(...)
cf_zone_id: str = Query(...)
webhook: str = Query("")
description: str = Query(..., min_length=0)
cost: int = Query(..., ge=0)
allowed_record_types: str = Query(...)
class CreateSubdomain(BaseModel):
domain: str = Query(...)
subdomain: str = Query(...)
email: str = Query(...)
ip: str = Query(...)
sats: int = Query(..., ge=0)
duration: int = Query(...)
record_type: str = Query(...)
class Domains(BaseModel):
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(BaseModel):
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View file

@ -1,65 +0,0 @@
import asyncio
import httpx
from loguru import logger
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .cloudflare import cloudflare_create_subdomain
from .crud import get_domain, set_subdomain_paid
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name())
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "lnsubdomain":
# not an lnsubdomain 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
try:
cf_response = await cloudflare_create_subdomain(
domain=domain, # type: ignore
subdomain=subdomain.subdomain,
record_type=subdomain.record_type,
ip=subdomain.ip,
)
except Exception as exc:
logger.error(exc)
logger.error("could not create subdomain on cloudflare")
return
### Use webhook to notify about cloudflare registration
if domain and 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,
)
assert r
except AssertionError:
pass

View file

@ -1,31 +0,0 @@
<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 />
<a
class="text-secondary"
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/subdomains"
>More details</a
>
<br />
<small>
Created by,
<a class="text-secondary" href="https://github.com/grmkris"
>Kris</a
></small
>
</p>
</q-card-section>
<q-btn flat label="Swagger API" type="a" href="../docs#/subdomains"></q-btn>
</q-card>
</q-expansion-item>

View file

@ -1,221 +0,0 @@
{% 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="primary"
: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 class="text-secondary" :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="'lightning:' + paymentReq.toUpperCase()"
: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 %}

View file

@ -1,549 +0,0 @@
{% 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="primary" @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>
<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">
{{SITE_TITLE}} 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>
<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="primary"
type="submit"
>Update Form</q-btn
>
<q-btn
v-else
unelevated
color="primary"
: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=true',
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',
'/subdomains/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=true',
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 =
typeof data.allowed_record_types === 'string'
? 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 %}

View file

@ -1,32 +0,0 @@
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 is 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

View file

@ -1,47 +0,0 @@
from http import HTTPStatus
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import subdomains_ext, subdomains_renderer
from .crud import get_domain
templates = Jinja2Templates(directory="templates")
@subdomains_ext.get("/", response_class=HTMLResponse)
async def index(
request: Request, user: User = Depends(check_user_exists) # type:ignore
):
return subdomains_renderer().TemplateResponse(
"subdomains/index.html", {"request": request, "user": user.dict()}
)
@subdomains_ext.get("/{domain_id}")
async def display(request: Request, domain_id):
domain = await get_domain(domain_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
allowed_records = (
domain.allowed_record_types.replace('"', "").replace(" ", "").split(",")
)
return subdomains_renderer().TemplateResponse(
"subdomains/display.html",
{
"request": request,
"domain_id": domain.id,
"domain_domain": domain.domain,
"domain_desc": domain.description,
"domain_cost": domain.cost,
"domain_allowed_record_types": allowed_records,
},
)

View file

@ -1,199 +0,0 @@
from http import HTTPStatus
from fastapi import Depends, Query
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import check_transaction_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type
from . import subdomains_ext
from .cloudflare import cloudflare_create_subdomain, cloudflare_deletesubdomain
from .crud import (
create_domain,
create_subdomain,
delete_domain,
delete_subdomain,
get_domain,
get_domains,
get_subdomain,
get_subdomainBySubdomain,
get_subdomains,
update_domain,
)
from .models import CreateDomain, CreateSubdomain
# domainS
@subdomains_ext.get("/api/v1/domains")
async def api_domains(
g: WalletTypeInfo = Depends(get_key_type),
all_wallets: bool = Query(False),
):
wallet_ids = [g.wallet.id]
if all_wallets:
user = await get_user(g.wallet.user)
if user is not None:
wallet_ids = user.wallet_ids
return [domain.dict() for domain in await get_domains(wallet_ids)]
@subdomains_ext.post("/api/v1/domains")
@subdomains_ext.put("/api/v1/domains/{domain_id}")
async def api_domain_create(
data: CreateDomain,
domain_id=None,
g: WalletTypeInfo = Depends(get_key_type),
):
if domain_id:
domain = await get_domain(domain_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
if domain.wallet != g.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your domain."
)
domain = await update_domain(domain_id, **data.dict())
else:
domain = await create_domain(data=data)
return domain.dict()
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
domain = await get_domain(domain_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
if domain.wallet != g.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain.")
await delete_domain(domain_id)
return "", HTTPStatus.NO_CONTENT
#########subdomains##########
@subdomains_ext.get("/api/v1/subdomains")
async def api_subdomains(
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [g.wallet.id]
if all_wallets:
user = await get_user(g.wallet.user)
if user is not None:
wallet_ids = user.wallet_ids
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
@subdomains_ext.post("/api/v1/subdomains/{domain_id}")
async def api_subdomain_make_subdomain(domain_id, data: CreateSubdomain):
domain = await get_domain(domain_id)
# If the request is coming for the non-existant domain
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNsubdomain does not exist."
)
## If record_type is not one of the allowed ones reject the request
if data.record_type not in domain.allowed_record_types:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{data.record_type} not a valid record.",
)
## If domain already exist in our database reject it
if await get_subdomainBySubdomain(data.subdomain) is not None:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{data.subdomain}.{domain.domain} domain already taken.",
)
## Dry run cloudflare... (create and if create is successful delete it)
try:
res_json = await cloudflare_create_subdomain(
domain=domain,
subdomain=data.subdomain,
record_type=data.record_type,
ip=data.ip,
)
await cloudflare_deletesubdomain(
domain=domain, domain_id=res_json["result"]["id"]
)
except Exception as exc:
logger.warning(exc)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Problem with cloudflare.",
)
## ALL OK - create an invoice and return it to the user
sats = data.sats
try:
payment_hash, payment_request = await create_invoice(
wallet_id=domain.wallet,
amount=sats,
memo=f"subdomain {data.subdomain}.{domain.domain} for {sats} sats for {data.duration} days",
extra={"tag": "lnsubdomain"},
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
subdomain = await create_subdomain(
payment_hash=payment_hash, wallet=domain.wallet, data=data
)
if not subdomain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNsubdomain could not be fetched."
)
return {"payment_hash": payment_hash, "payment_request": payment_request}
@subdomains_ext.get("/api/v1/subdomains/{payment_hash}")
async def api_subdomain_send_subdomain(payment_hash):
subdomain = await get_subdomain(payment_hash)
assert subdomain
try:
status = await check_transaction_status(subdomain.wallet, payment_hash)
is_paid = not status.pending
except Exception:
return {"paid": False}
if is_paid:
return {"paid": True}
return {"paid": False}
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key_type)):
subdomain = await get_subdomain(subdomain_id)
if not subdomain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNsubdomain does not exist."
)
if subdomain.wallet != g.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your subdomain."
)
await delete_subdomain(subdomain_id)
return "", HTTPStatus.NO_CONTENT