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
|
||||
|
||||
|
||||
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:
|
||||
await db.execute(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ from fastapi.param_functions import Query
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class RotateAddressData(BaseModel):
|
||||
pubkey: str
|
||||
|
||||
|
||||
class CreateAddressData(BaseModel):
|
||||
domain_id: str
|
||||
local_part: str
|
||||
|
|
|
|||
|
|
@ -118,6 +118,16 @@
|
|||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<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
|
||||
unelevated
|
||||
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 %}
|
||||
<link rel="stylesheet" href="/nostrnip5/static/css/signup.css" />
|
||||
<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">
|
||||
<p>
|
||||
You can use this page to get NIP-5 verified on the nostr protocol under
|
||||
|
|
@ -51,7 +76,6 @@ context %} {% block page %}
|
|||
type="submit"
|
||||
>Create Address</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
|
|
@ -90,11 +114,17 @@ context %} {% block page %}
|
|||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
base_url: '{{ request.base_url }}',
|
||||
domain: '{{ domain.domain }}',
|
||||
domain_id: '{{ domain_id }}',
|
||||
wallet: '{{ domain.wallet }}',
|
||||
currency: '{{ domain.currency }}',
|
||||
amount: '{{ domain.amount }}',
|
||||
success: false,
|
||||
successData: {
|
||||
local_part: null,
|
||||
address_id: null
|
||||
},
|
||||
qrCodeDialog: {
|
||||
data: {
|
||||
payment_request: null
|
||||
|
|
@ -155,11 +185,9 @@ context %} {% block page %}
|
|||
qrCodeDialog.dismissMsg()
|
||||
qrCodeDialog.show = false
|
||||
|
||||
setTimeout(function () {
|
||||
alert(
|
||||
`Success! Your username is now active at ${localPart}@${self.domain}. Please add this to your nostr profile accordingly.`
|
||||
)
|
||||
}, 500)
|
||||
self.successData.local_part = localPart
|
||||
self.successData.address_id = qrCodeDialog.data.address_id
|
||||
self.success = true
|
||||
}
|
||||
})
|
||||
}, 3000)
|
||||
|
|
@ -168,9 +196,7 @@ context %} {% block page %}
|
|||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
created: function () {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from lnbits.core.models import User
|
|||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import nostrnip5_ext, nostrnip5_renderer
|
||||
from .crud import get_domain
|
||||
from .crud import get_address, get_domain
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
|
@ -40,3 +40,30 @@ async def index(request: Request, domain_id: str):
|
|||
"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_by_name,
|
||||
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)
|
||||
|
|
@ -113,6 +114,31 @@ async def api_address_activate(
|
|||
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(
|
||||
"/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(
|
||||
wallet_id=domain.wallet,
|
||||
amount=price_in_sats,
|
||||
memo=f"Payment for domain {domain_id}",
|
||||
memo=f"Payment for NIP-05 for {address.local_part}@{domain.domain}",
|
||||
extra={
|
||||
"tag": "nostrnip5",
|
||||
"domain_id": domain_id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue