feat: DID management UI in Federation — rotate DID + notify peers
- "My Node Identity" card shows DID with copy button - "Rotate DID" button opens modal with password confirmation - Rotation generates new keypair, then auto-notifies all federation peers - Shows success/failure count after notification Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,42 @@
|
||||
<p class="text-sm text-white/60 mt-2">{{ nodes.length }} federated node{{ nodes.length !== 1 ? 's' : '' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- My Node Identity -->
|
||||
<div v-if="selfDid" class="glass-card p-4 mb-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-xs text-white/40 mb-1">Your Node DID</p>
|
||||
<p class="text-sm text-white/70 font-mono truncate cursor-pointer" :title="selfDid" @click="copyDid">{{ selfDid }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button @click="copyDid" class="glass-button px-3 py-1.5 rounded-lg text-xs">Copy</button>
|
||||
<button @click="showRotateModal = true" class="glass-button px-3 py-1.5 rounded-lg text-xs text-orange-300">Rotate DID</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rotate DID Modal -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="showRotateModal" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click.self="showRotateModal = false">
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div class="glass-card p-6 max-w-md w-full relative z-10">
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Rotate Node DID</h3>
|
||||
<p class="text-sm text-white/60 mb-4">This generates a new identity keypair and notifies all federated peers. Your old DID will no longer be valid.</p>
|
||||
<input v-model="rotatePassword" type="password" placeholder="Enter your password to confirm" class="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-white/30 focus:outline-none focus:border-orange-500/50 mb-4" />
|
||||
<p v-if="rotateError" class="text-red-400 text-xs mb-3">{{ rotateError }}</p>
|
||||
<p v-if="rotateSuccess" class="text-green-400 text-xs mb-3">{{ rotateSuccess }}</p>
|
||||
<div class="flex gap-3">
|
||||
<button @click="showRotateModal = false; rotatePassword = ''; rotateError = ''; rotateSuccess = ''" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
|
||||
<button @click="rotateDid" :disabled="rotatingDid || !rotatePassword" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30 disabled:opacity-50">
|
||||
{{ rotatingDid ? 'Rotating...' : 'Rotate & Notify Peers' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- View Tabs -->
|
||||
<div v-if="nodes.length > 0" class="flex gap-1 mb-6 p-1 bg-black/20 rounded-lg w-fit">
|
||||
<button
|
||||
@@ -679,6 +715,52 @@ function formatBytes(bytes?: number): string {
|
||||
return val.toFixed(1) + ' ' + units[i]
|
||||
}
|
||||
|
||||
// DID rotation
|
||||
const showRotateModal = ref(false)
|
||||
const rotatePassword = ref('')
|
||||
const rotatingDid = ref(false)
|
||||
const rotateError = ref('')
|
||||
const rotateSuccess = ref('')
|
||||
|
||||
function copyDid() {
|
||||
if (selfDid.value) {
|
||||
navigator.clipboard.writeText(selfDid.value).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
async function rotateDid() {
|
||||
if (!rotatePassword.value) return
|
||||
rotatingDid.value = true
|
||||
rotateError.value = ''
|
||||
rotateSuccess.value = ''
|
||||
try {
|
||||
const result = await rpcClient.call<{
|
||||
old_did: string; new_did: string; proof_signature: string; proof_message: string
|
||||
}>({ method: 'node.rotate-did', params: { password: rotatePassword.value } })
|
||||
|
||||
selfDid.value = result.new_did
|
||||
rotateSuccess.value = `DID rotated. Notifying peers...`
|
||||
|
||||
// Notify federation peers
|
||||
const notify = await rpcClient.call<{ notified: number; failed: number }>({
|
||||
method: 'federation.notify-did-change',
|
||||
params: {
|
||||
old_did: result.old_did,
|
||||
new_did: result.new_did,
|
||||
proof_signature: result.proof_signature,
|
||||
proof_message: result.proof_message,
|
||||
},
|
||||
timeout: 120000,
|
||||
})
|
||||
rotateSuccess.value = `DID rotated successfully. ${notify.notified} peers notified${notify.failed > 0 ? `, ${notify.failed} failed` : ''}.`
|
||||
rotatePassword.value = ''
|
||||
} catch (err: unknown) {
|
||||
rotateError.value = err instanceof Error ? err.message : 'Rotation failed'
|
||||
} finally {
|
||||
rotatingDid.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatUptime(secs: number): string {
|
||||
const days = Math.floor(secs / 86400)
|
||||
const hours = Math.floor((secs % 86400) / 3600)
|
||||
|
||||
Reference in New Issue
Block a user