add self-service pubkey rotation for nip-05 addresses
This commit is contained in:
parent
6996155cae
commit
e65082b585
7 changed files with 214 additions and 13 deletions
|
|
@ -101,6 +101,26 @@ async def activate_address(domain_id: str, address_id: str) -> Address:
|
||||||
return address
|
return address
|
||||||
|
|
||||||
|
|
||||||
|
async def rotate_address(domain_id: str, address_id: str, pubkey: str) -> Address:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE nostrnip5.addresses
|
||||||
|
SET pubkey = ?
|
||||||
|
WHERE domain_id = ?
|
||||||
|
AND id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
pubkey,
|
||||||
|
domain_id,
|
||||||
|
address_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
address = await get_address(domain_id, address_id)
|
||||||
|
assert address, "Newly updated address couldn't be retrieved"
|
||||||
|
return address
|
||||||
|
|
||||||
|
|
||||||
async def delete_domain(domain_id) -> bool:
|
async def delete_domain(domain_id) -> bool:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ from fastapi.param_functions import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class RotateAddressData(BaseModel):
|
||||||
|
pubkey: str
|
||||||
|
|
||||||
|
|
||||||
class CreateAddressData(BaseModel):
|
class CreateAddressData(BaseModel):
|
||||||
domain_id: str
|
domain_id: str
|
||||||
local_part: str
|
local_part: str
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,16 @@
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="edit"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
type="a"
|
||||||
|
target="_blank"
|
||||||
|
:href="'rotate/' + props.row.domain_id + '/' + props.row.id"
|
||||||
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
88
lnbits/extensions/nostrnip5/templates/nostrnip5/rotate.html
Normal file
88
lnbits/extensions/nostrnip5/templates/nostrnip5/rotate.html
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
{% extends "public.html" %} {% block toolbar_title %} Rotate Keys For {{
|
||||||
|
domain.domain }} {% endblock %} {% from "macros.jinja" import window_vars with
|
||||||
|
context %} {% block page %}
|
||||||
|
<link rel="stylesheet" href="/nostrnip5/static/css/signup.css" />
|
||||||
|
<div>
|
||||||
|
<q-card class="q-pa-lg q-pt-lg">
|
||||||
|
<q-form @submit="updateAddress" class="q-gutter-md">
|
||||||
|
<p>
|
||||||
|
You can use this page to change the public key associated with your
|
||||||
|
NIP-5 identity.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Your current NIP-5 identity is {{ address.local_part }}@{{ domain.domain
|
||||||
|
}} with nostr public key {{ address.pubkey }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Input your new pubkey below to update it.</p>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.pubkey"
|
||||||
|
label="Pub Key"
|
||||||
|
placeholder="abc234"
|
||||||
|
:rules="[ val => val.length = 64 || val.indexOf('npub') === 0 ||'Please enter a hex pubkey' ]"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="formDialog.data.pubkey == null"
|
||||||
|
type="submit"
|
||||||
|
>Rotate Keys</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
domain: '{{ domain.domain }}',
|
||||||
|
domain_id: '{{ domain_id }}',
|
||||||
|
address_id: '{{ address_id }}',
|
||||||
|
formDialog: {
|
||||||
|
data: {
|
||||||
|
pubkey: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateAddress: function () {
|
||||||
|
var self = this
|
||||||
|
var formDialog = this.formDialog
|
||||||
|
var newPubKey = this.formDialog.data.pubkey
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
'/nostrnip5/api/v1/domain/' +
|
||||||
|
this.domain_id +
|
||||||
|
'/address/' +
|
||||||
|
this.address_id +
|
||||||
|
'/rotate',
|
||||||
|
formDialog.data
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
formDialog.data = {}
|
||||||
|
alert(
|
||||||
|
`Success! Your pubkey has been updated. Please allow clients time to refresh the data.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,7 +3,32 @@ domain.domain }} {% endblock %} {% from "macros.jinja" import window_vars with
|
||||||
context %} {% block page %}
|
context %} {% block page %}
|
||||||
<link rel="stylesheet" href="/nostrnip5/static/css/signup.css" />
|
<link rel="stylesheet" href="/nostrnip5/static/css/signup.css" />
|
||||||
<div>
|
<div>
|
||||||
<q-card class="q-pa-lg q-pt-lg">
|
<q-card class="q-pa-lg q-pt-lg" v-if="success == true">
|
||||||
|
{% raw %}
|
||||||
|
<p>
|
||||||
|
Success! Your username is now active at {{ successData.local_part }}@{{
|
||||||
|
domain }}. Please add this to your nostr profile accordingly. If you ever
|
||||||
|
need to rotate your keys, you can still keep your identity!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Important!</h3>
|
||||||
|
<p>
|
||||||
|
Bookmark this link:
|
||||||
|
<a
|
||||||
|
v-bind:href="'/nostrnip5/rotate/' + domain_id + '/' + successData.address_id"
|
||||||
|
target="_blank"
|
||||||
|
>{{ base_url }}nostrnip5/rotate/{{ domain_id }}/{{
|
||||||
|
successData.address_id }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In case you ever need to change your pubkey, you can still keep this NIP-5
|
||||||
|
identity. Just come back to the above linked page to change the pubkey
|
||||||
|
associated to your identity.
|
||||||
|
</p>
|
||||||
|
{% endraw %}
|
||||||
|
</q-card>
|
||||||
|
<q-card class="q-pa-lg q-pt-lg" v-if="success == false">
|
||||||
<q-form @submit="createAddress" class="q-gutter-md">
|
<q-form @submit="createAddress" class="q-gutter-md">
|
||||||
<p>
|
<p>
|
||||||
You can use this page to get NIP-5 verified on the nostr protocol under
|
You can use this page to get NIP-5 verified on the nostr protocol under
|
||||||
|
|
@ -51,7 +76,6 @@ context %} {% block page %}
|
||||||
type="submit"
|
type="submit"
|
||||||
>Create Address</q-btn
|
>Create Address</q-btn
|
||||||
>
|
>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -90,11 +114,17 @@ context %} {% block page %}
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
base_url: '{{ request.base_url }}',
|
||||||
domain: '{{ domain.domain }}',
|
domain: '{{ domain.domain }}',
|
||||||
domain_id: '{{ domain_id }}',
|
domain_id: '{{ domain_id }}',
|
||||||
wallet: '{{ domain.wallet }}',
|
wallet: '{{ domain.wallet }}',
|
||||||
currency: '{{ domain.currency }}',
|
currency: '{{ domain.currency }}',
|
||||||
amount: '{{ domain.amount }}',
|
amount: '{{ domain.amount }}',
|
||||||
|
success: false,
|
||||||
|
successData: {
|
||||||
|
local_part: null,
|
||||||
|
address_id: null
|
||||||
|
},
|
||||||
qrCodeDialog: {
|
qrCodeDialog: {
|
||||||
data: {
|
data: {
|
||||||
payment_request: null
|
payment_request: null
|
||||||
|
|
@ -155,11 +185,9 @@ context %} {% block page %}
|
||||||
qrCodeDialog.dismissMsg()
|
qrCodeDialog.dismissMsg()
|
||||||
qrCodeDialog.show = false
|
qrCodeDialog.show = false
|
||||||
|
|
||||||
setTimeout(function () {
|
self.successData.local_part = localPart
|
||||||
alert(
|
self.successData.address_id = qrCodeDialog.data.address_id
|
||||||
`Success! Your username is now active at ${localPart}@${self.domain}. Please add this to your nostr profile accordingly.`
|
self.success = true
|
||||||
)
|
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
@ -168,9 +196,7 @@ context %} {% block page %}
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
computed: {},
|
|
||||||
created: function () {}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
from . import nostrnip5_ext, nostrnip5_renderer
|
from . import nostrnip5_ext, nostrnip5_renderer
|
||||||
from .crud import get_domain
|
from .crud import get_address, get_domain
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
@ -40,3 +40,30 @@ async def index(request: Request, domain_id: str):
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrnip5_ext.get("/rotate/{domain_id}/{address_id}", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request, domain_id: str, address_id: str):
|
||||||
|
domain = await get_domain(domain_id)
|
||||||
|
address = await get_address(domain_id, address_id)
|
||||||
|
|
||||||
|
if not domain:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not address:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Address does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
return nostrnip5_renderer().TemplateResponse(
|
||||||
|
"nostrnip5/rotate.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"domain_id": domain_id,
|
||||||
|
"domain": domain,
|
||||||
|
"address_id": address_id,
|
||||||
|
"address": address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ from .crud import (
|
||||||
get_domain,
|
get_domain,
|
||||||
get_domain_by_name,
|
get_domain_by_name,
|
||||||
get_domains,
|
get_domains,
|
||||||
|
rotate_address,
|
||||||
)
|
)
|
||||||
from .models import CreateAddressData, CreateDomainData
|
from .models import CreateAddressData, CreateDomainData, RotateAddressData
|
||||||
|
|
||||||
|
|
||||||
@nostrnip5_ext.get("/api/v1/domains", status_code=HTTPStatus.OK)
|
@nostrnip5_ext.get("/api/v1/domains", status_code=HTTPStatus.OK)
|
||||||
|
|
@ -113,6 +114,31 @@ async def api_address_activate(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@nostrnip5_ext.post(
|
||||||
|
"/api/v1/domain/{domain_id}/address/{address_id}/rotate",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
)
|
||||||
|
async def api_address_rotate(
|
||||||
|
domain_id: str,
|
||||||
|
address_id: str,
|
||||||
|
post_data: RotateAddressData,
|
||||||
|
):
|
||||||
|
|
||||||
|
if post_data.pubkey.startswith("npub"):
|
||||||
|
hrp, data = bech32_decode(post_data.pubkey)
|
||||||
|
decoded_data = convertbits(data, 5, 8, False)
|
||||||
|
post_data.pubkey = bytes(decoded_data).hex()
|
||||||
|
|
||||||
|
if len(bytes.fromhex(post_data.pubkey)) != 32:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Pubkey must be in hex format."
|
||||||
|
)
|
||||||
|
|
||||||
|
await rotate_address(domain_id, address_id, post_data.pubkey)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@nostrnip5_ext.post(
|
@nostrnip5_ext.post(
|
||||||
"/api/v1/domain/{domain_id}/address", status_code=HTTPStatus.CREATED
|
"/api/v1/domain/{domain_id}/address", status_code=HTTPStatus.CREATED
|
||||||
)
|
)
|
||||||
|
|
@ -156,7 +182,7 @@ async def api_address_create(
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=domain.wallet,
|
wallet_id=domain.wallet,
|
||||||
amount=price_in_sats,
|
amount=price_in_sats,
|
||||||
memo=f"Payment for domain {domain_id}",
|
memo=f"Payment for NIP-05 for {address.local_part}@{domain.domain}",
|
||||||
extra={
|
extra={
|
||||||
"tag": "nostrnip5",
|
"tag": "nostrnip5",
|
||||||
"domain_id": domain_id,
|
"domain_id": domain_id,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue