add self-service pubkey rotation for nip-05 addresses

This commit is contained in:
Lee Salminen 2022-12-21 14:05:45 -06:00
parent 6996155cae
commit e65082b585
7 changed files with 214 additions and 13 deletions

View file

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

View file

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

View file

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

View 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 %}

View file

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

View file

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

View file

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