started working on subdomains extension

This commit is contained in:
Kristjan 2020-12-28 19:51:45 +01:00
parent 1c922a5ddc
commit 3c398a8276
8 changed files with 489 additions and 0 deletions

View File

@ -0,0 +1,65 @@
<h1>Subdomains Extension</h1>
#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
<h2>If your extension has API endpoints, include useful ones here</h2>
<code>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"</code>
## 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
}'
```

View File

@ -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

View File

@ -0,0 +1,6 @@
{
"name": "Subdomains",
"short_description": "Sell subdomains of your domain",
"icon": "domain",
"contributors": ["grmkris"]
}

View File

@ -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'))
);
"""
)

View File

@ -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

View File

@ -0,0 +1,296 @@
{% 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>
</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-input filled dense v-model.trim="domainDialog.data.domainName" type="name" label="Domain name "></q-input>
<q-input filled dense v-model.trim="domainDialog.data.cfToken" type="text" label="Cloudflare API token">
</q-input>
<q-input filled dense v-model.trim="domainDialog.data.cfZoneId" 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"></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.domainName == 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.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/subdomains/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
domains: [],
domainsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'domainName', align: 'left', label: 'Domain name', field: 'name'},
{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
},
},
domainDialog: {
show: false,
data: {}
}
}
},
methods: {
getSubdomains: function () {
var self = this
/*
LNbits.api
.request(
'GET',
'/lnticket/api/v1/tickets?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tickets = response.data.map(function (obj) {
return mapLNTicket(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
)
.then(function (response) {
self.tickets = _.reject(self.tickets, function (obj) {
return obj.id == ticketId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
*/
},
exportSubdomainsCSV: function () {
LNbits.utils.exportCSV(this.domainsTable.columns, this.tickets)
},
getDomains: function () {
var self = this
/*
LNbits.api
.request(
'GET',
'/lnticket/api/v1/forms?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.forms = response.data.map(function (obj) {
return mapLNTicket(obj)
})
})
*/
},
sendFormData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.domainDialog.data.wallet
})
var data = this.domainDialog.data
if (data.id) {
this.updateForm(wallet, data)
} else {
this.createForm(wallet, data)
}
},
createDomain: function (wallet, data) {
var self = this
/*
LNbits.api
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
.then(function (response) {
self.forms.push(mapLNTicket(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})
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.description = link.description
this.domainDialog.domainDialog.data.cfToken = link.cfToken
this.domainDialog.cfZoneId = link.cfZoneId
this.domainDialog.data.cost = link.cost
this.domainDialog.show = true
},
updateDomain: function (wallet, data) {
var self = this
console.log(data)
/*
LNbits.api
.request(
'PUT',
'/lnticket/api/v1/forms/' + data.id,
wallet.inkey,
data
)
.then(function (response) {
self.forms = _.reject(self.forms, function (obj) {
return obj.id == data.id
})
self.forms.push(mapLNTicket(response.data))
self.domainDialog.show = false
self.domainDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
*/
},
deleteDomain: function (formsId) {
var self = this
var forms = _.findWhere(this.forms, {id: formsId})
/*
LNbits.utils
.confirmDialog('Are you sure you want to delete this form link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnticket/api/v1/forms/' + formsId,
_.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey
)
.then(function (response) {
self.forms = _.reject(self.forms, function (obj) {
return obj.id == formsId
})
})
.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

@ -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)

View File

@ -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