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:
Dorian
2026-03-19 19:31:03 +00:00
parent 1a138c0409
commit cf184661d9

View File

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