feat: add optional domain field (#120)

* feat: add optional domain field
closes #119
This commit is contained in:
dni ⚡ 2026-01-15 09:20:54 +01:00 committed by GitHub
commit 17135b45ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 107 additions and 46 deletions

View file

@ -65,6 +65,7 @@ async def create_pay_link(data: CreatePayLinkData) -> PayLink:
created_at=now, created_at=now,
updated_at=now, updated_at=now,
disposable=data.disposable if data.disposable is not None else True, disposable=data.disposable if data.disposable is not None else True,
domain=data.domain,
) )
await db.insert("lnurlp.pay_links", link) await db.insert("lnurlp.pay_links", link)

View file

@ -10,11 +10,16 @@ def parse_nostr_private_key(key: str) -> PrivateKey:
return PrivateKey(bytes.fromhex(key)) return PrivateKey(bytes.fromhex(key))
def lnurl_encode_link_id(req: Request, link_id: str) -> str: def lnurl_encode_link(req: Request, link_id: str, domain: str | None = None) -> str:
if domain:
url_str = f"https://{domain}/lnurlp/{link_id}"
return str(lnurl_encode(url_str).bech32)
url = req.url_for("lnurlp.api_lnurl_response", link_id=link_id) url = req.url_for("lnurlp.api_lnurl_response", link_id=link_id)
url = url.replace(path=url.path) url = url.replace(path=url.path)
url_str = str(url) url_str = str(url)
if url.netloc.endswith(".onion"): if url.netloc.endswith(".onion"):
# change url string scheme to http # change url string scheme to http
url_str = url_str.replace("https://", "http://") url_str = url_str.replace("https://", "http://")
return str(lnurl_encode(url_str).bech32) return str(lnurl_encode(url_str).bech32)

View file

@ -35,6 +35,7 @@ class CreatePayLinkData(BaseModel):
username: str | None = Query(None) username: str | None = Query(None)
zaps: bool | None = Query(False) zaps: bool | None = Query(False)
disposable: bool | None = Query(True) disposable: bool | None = Query(True)
domain: str | None = Query(None)
class PayLink(BaseModel): class PayLink(BaseModel):
@ -67,8 +68,25 @@ class PayLink(BaseModel):
success_url: str | None = None success_url: str | None = None
currency: str | None = None currency: str | None = None
fiat_base_multiplier: int | None = None fiat_base_multiplier: int | None = None
disposable: bool disposable: bool
# TODO deprecated, unused in the code, should be deleted from db.
domain: str | None = None domain: str | None = None
class PublicPayLink(BaseModel):
id: str
username: str | None = None
description: str
min: float
max: float
domain: str | None = None
currency: str | None = None
lnurl: str | None = Field(
default=None,
no_database=True,
deprecated=True,
description=(
"Deprecated: Instead of using this bech32 encoded string, dynamically "
"generate your own static link (lud17/bech32) on the client side. "
"Example: lnurlp://${window.location.hostname}/lnurlp/${paylink_id}"
),
)

View file

@ -2,10 +2,26 @@ window.PageLnurlpPublic = {
template: '#page-lnurlp-public', template: '#page-lnurlp-public',
data() { data() {
return { return {
url: '' url: '',
payLink: null
}
},
methods: {
setUrl(link_id, domain) {
this.url = `https://${domain || window.location.host}/lnurlp/${link_id}`
},
getPayLink() {
this.api
.request('GET', `/lnurlp/api/v1/links/public/${this.$route.params.id}`)
.then(res => {
this.payLink = res.data
this.setUrl(this.payLink.id, this.payLink.domain)
})
.catch(this.utils.notifyApiError)
} }
}, },
created() { created() {
this.url = window.location.origin + '/lnurlp/' + this.$route.params.id this.setUrl(this.$route.params.id)
this.getPayLink()
} }
} }

View file

@ -84,6 +84,10 @@ window.PageLnurlp = {
} }
}, },
methods: { methods: {
lnaddress(link) {
const domain = link.domain || window.location.host
return `${link.username}@${domain}`
},
mapPayLink(obj) { mapPayLink(obj) {
const locationPath = [ const locationPath = [
window.location.protocol, window.location.protocol,
@ -140,11 +144,13 @@ window.PageLnurlp = {
(link.success_url ? ' and URL "' + link.success_url + '"' : '') (link.success_url ? ' and URL "' + link.success_url + '"' : '')
: 'do nothing', : 'do nothing',
lnurl: link.lnurl, lnurl: link.lnurl,
domain: link.domain,
pay_url: link.pay_url, pay_url: link.pay_url,
print_url: link.print_url, print_url: link.print_url,
username: link.username username: link.username
} }
this.activeUrl = window.location.origin + '/lnurlp/' + link.id const domain = link.domain || window.location.host
this.activeUrl = `https://${domain}/lnurlp//${link.id}`
this.qrCodeDialog.show = true this.qrCodeDialog.show = true
}, },
openUpdateDialog(linkId) { openUpdateDialog(linkId) {

View file

@ -36,7 +36,6 @@
<span v-text="col.label"></span> <span v-text="col.label"></span>
</q-th> </q-th>
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th auto-width></q-th>
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <template v-slot:body="props">
@ -55,7 +54,6 @@
><q-tooltip>Shareable Page</q-tooltip></q-btn ><q-tooltip>Shareable Page</q-tooltip></q-btn
> >
<q-btn <q-btn
unelevated
dense dense
size="xs" size="xs"
icon="visibility" icon="visibility"
@ -64,6 +62,27 @@
@click="openQrCodeDialog(props.row.id)" @click="openQrCodeDialog(props.row.id)"
><q-tooltip>View Link</q-tooltip></q-btn ><q-tooltip>View Link</q-tooltip></q-btn
> >
<q-btn
flat
dense
size="xs"
@click="openUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
class="q-ml-sm"
>
<q-tooltip>Edit</q-tooltip>
</q-btn>
<q-btn
flat
dense
size="xs"
@click="deletePayLink(props.row.id)"
icon="cancel"
color="pink"
class="q-ml-sm"
><q-tooltip>Delete</q-tooltip></q-btn
>
</q-td> </q-td>
<q-td <q-td
v-for="col in props.cols" v-for="col in props.cols"
@ -104,27 +123,6 @@
</q-tooltip> </q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
>
<q-tooltip>Edit</q-tooltip>
</q-btn>
<q-btn
flat
dense
size="xs"
@click="deletePayLink(props.row.id)"
icon="cancel"
color="pink"
><q-tooltip>Delete</q-tooltip></q-btn
>
</q-td>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
@ -402,10 +400,17 @@
" "
/> />
</div> </div>
<div class="col" style="margin-top: 10px"> <div class="col" style="flex: 0 0 auto; margin-top: 10px">
<span class="label"> <span class="label"> &nbsp;@&nbsp; </span>
&nbsp;@&nbsp;<span v-text="domain"></span> </div>
</span> <div class="col">
<q-input
filled
dense
v-model.trim="formDialog.data.domain"
type="text"
:label="domain"
/>
</div> </div>
</div> </div>
<div class="row q-col-gutter-sm q-mx-sm"> <div class="row q-col-gutter-sm q-mx-sm">
@ -642,7 +647,7 @@
<span v-text="qrCodeDialog.data.success"></span><br /> <span v-text="qrCodeDialog.data.success"></span><br />
<span v-if="qrCodeDialog.data.username"> <span v-if="qrCodeDialog.data.username">
<strong>Lightning Address: </strong> <strong>Lightning Address: </strong>
<span v-text="qrCodeDialog.data.username + '@' + domain"></span> <span v-text="lnaddress(qrCodeDialog.data)"></span>
<br /> <br />
</span> </span>
</p> </p>

View file

@ -23,15 +23,15 @@ from .crud import (
update_lnurlp_settings, update_lnurlp_settings,
update_pay_link, update_pay_link,
) )
from .helpers import lnurl_encode_link_id, parse_nostr_private_key from .helpers import lnurl_encode_link, parse_nostr_private_key
from .models import CreatePayLinkData, LnurlpSettings, PayLink from .models import CreatePayLinkData, LnurlpSettings, PayLink, PublicPayLink
lnurlp_api_router = APIRouter() lnurlp_api_router = APIRouter()
def check_lnurl_encode(req: Request, link_id: str) -> str: def check_lnurl_encode(req: Request, link: PayLink) -> str:
try: try:
return lnurl_encode_link_id(req, link_id) return lnurl_encode_link(req, link.id, link.domain)
except InvalidUrl as exc: except InvalidUrl as exc:
raise HTTPException( raise HTTPException(
detail=( detail=(
@ -60,11 +60,11 @@ async def api_links(
links = await get_pay_links(wallet_ids) links = await get_pay_links(wallet_ids)
for link in links: for link in links:
link.lnurl = check_lnurl_encode(req=req, link_id=link.id) link.lnurl = check_lnurl_encode(req, link)
return links return links
@lnurlp_api_router.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @lnurlp_api_router.get("/api/v1/links/{link_id}")
async def api_link_retrieve( async def api_link_retrieve(
req: Request, link_id: str, key_info: WalletTypeInfo = Depends(require_invoice_key) req: Request, link_id: str, key_info: WalletTypeInfo = Depends(require_invoice_key)
) -> PayLink: ) -> PayLink:
@ -85,7 +85,18 @@ async def api_link_retrieve(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
) )
link.lnurl = check_lnurl_encode(req, link.id) link.lnurl = check_lnurl_encode(req, link)
return link
@lnurlp_api_router.get("/api/v1/links/public/{link_id}", response_model=PublicPayLink)
async def api_link_public_retrieve(req: Request, link_id: str) -> PayLink:
link = await get_pay_link(link_id)
if not link:
raise HTTPException(
detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
)
link.lnurl = lnurl_encode_link(req, link.id, link.domain)
return link return link
@ -168,7 +179,7 @@ async def api_link_create_or_update(
detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN
) )
# admins are allowed to create/edit paylinks beloging to regular users # admins are allowed to create/edit paylinks belonging to regular users
user = await get_user(key_info.wallet.user) user = await get_user(key_info.wallet.user)
admin_user = user.admin if user else False admin_user = user.admin if user else False
if not admin_user and new_wallet.user != key_info.wallet.user: if not admin_user and new_wallet.user != key_info.wallet.user:
@ -197,8 +208,7 @@ async def api_link_create_or_update(
link = await create_pay_link(data) link = await create_pay_link(data)
link.lnurl = check_lnurl_encode(req=req, link_id=link.id) link.lnurl = check_lnurl_encode(req, link)
return link return link

View file

@ -99,7 +99,7 @@ async def api_lnurl_callback(
extra["nostr"] = nostr # put it here for later publishing in tasks.py extra["nostr"] = nostr # put it here for later publishing in tasks.py
if link.username: if link.username:
identifier = f"{link.username}@{request.url.netloc}" identifier = f"{link.username}@{link.domain or request.url.netloc}"
text = f"Payment to {link.username}" text = f"Payment to {link.username}"
_metadata = [["text/plain", text], ["text/identifier", identifier]] _metadata = [["text/plain", text], ["text/identifier", identifier]]
extra["lnaddress"] = identifier extra["lnaddress"] = identifier
@ -173,7 +173,7 @@ async def api_lnurl_response(
callback_url = parse_obj_as(CallbackUrl, str(url)) callback_url = parse_obj_as(CallbackUrl, str(url))
if link.username: if link.username:
identifier = f"{link.username}@{request.url.netloc}" identifier = f"{link.username}@{link.domain or request.url.netloc}"
text = f"Payment to {link.username}" text = f"Payment to {link.username}"
metadata = [["text/plain", text], ["text/identifier", identifier]] metadata = [["text/plain", text], ["text/identifier", identifier]]
else: else: