Update Fedimint configuration and enhance onboarding process
- Upgraded Fedimint version to v0.10.0 in docker-compose.yml and manifest.yml, adding support for the built-in Guardian UI. - Modified .gitignore to exclude deploy-config.sh script. - Enhanced onboarding process in AuthManager to persist onboarding state and validate password strength during user setup. - Updated API to handle onboarding completion and password change requests, ensuring a smoother user experience. - Improved configuration management to support Nostr discovery and Tor proxy settings, enhancing node identity features.
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">Settings</h1>
|
||||
<p class="text-white/80">Configure your Archipelago experience</p>
|
||||
<div class="mb-8 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">Settings</h1>
|
||||
<p class="text-white/80">Configure your Archipelago experience</p>
|
||||
</div>
|
||||
<!-- Controller indicator - Mobile only (desktop shows in sidebar) -->
|
||||
<div class="md:hidden">
|
||||
<ControllerIndicator />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Section -->
|
||||
@@ -43,8 +49,131 @@
|
||||
</div>
|
||||
<p class="text-base font-medium text-white/90">Currently logged in</p>
|
||||
</div>
|
||||
|
||||
<!-- Identity Card: DID + Tor Address (onion below DID, with copy) -->
|
||||
<div v-if="userDid || serverTorAddress" class="bg-black/20 rounded-xl px-5 py-4 border border-white/10 md:col-span-2 space-y-4">
|
||||
<!-- DID -->
|
||||
<div v-if="userDid">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">Your DID</p>
|
||||
</div>
|
||||
<p class="text-sm font-mono text-white/90 break-all" :title="userDid">{{ userDid }}</p>
|
||||
<p class="text-xs text-white/50 mt-1">Decentralized identifier for passwordless auth</p>
|
||||
</div>
|
||||
|
||||
<!-- Tor / Onion Address (below DID, with copy button) -->
|
||||
<div v-if="serverTorAddress" :class="userDid ? 'pt-4 border-t border-white/10' : ''">
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">Node .onion Address</p>
|
||||
</div>
|
||||
<button
|
||||
@click="copyOnionAddress"
|
||||
class="shrink-0 px-3 py-1.5 rounded-lg glass-button glass-button-sm text-xs font-medium text-white/90 hover:text-white transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<svg v-if="!copiedOnion" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span v-else class="text-green-400 text-xs">Copied</span>
|
||||
<span v-if="!copiedOnion">Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm font-mono text-amber-400/90 break-all" :title="serverTorAddress">{{ serverTorAddress }}</p>
|
||||
<p class="text-xs text-white/50 mt-1">Onion address for node interface and peer discovery over Tor</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password -->
|
||||
<div class="mb-6">
|
||||
<button
|
||||
@click="showChangePasswordModal = true"
|
||||
class="w-full flex items-center justify-center gap-2 mb-4 px-4 py-2 rounded-lg border border-orange-500/50 text-orange-400 font-medium hover:bg-orange-500/10 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span>Change Password</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showChangePasswordModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showChangePasswordModal = false"
|
||||
>
|
||||
<div class="glass-card p-6 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Change Password</h3>
|
||||
<p class="text-white/70 text-sm mb-4">Updates both web login and SSH access. Use a strong password (12+ chars, upper, lower, digit, special).</p>
|
||||
<form @submit.prevent="handleChangePassword" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/80 mb-2">Current Password</label>
|
||||
<input
|
||||
v-model="changePasswordForm.currentPassword"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/80 mb-2">New Password</label>
|
||||
<input
|
||||
v-model="changePasswordForm.newPassword"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
||||
placeholder="12+ chars, upper, lower, digit, special"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/80 mb-2">Confirm New Password</label>
|
||||
<input
|
||||
v-model="changePasswordForm.confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
||||
placeholder="Re-enter new password"
|
||||
/>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm text-white/80">
|
||||
<input v-model="changePasswordForm.alsoChangeSsh" type="checkbox" class="rounded border-white/30" />
|
||||
Also update SSH password (recommended)
|
||||
</label>
|
||||
<p v-if="changePasswordError" class="text-sm text-red-400">{{ changePasswordError }}</p>
|
||||
<p v-if="changePasswordSuccess" class="text-sm text-green-400">{{ changePasswordSuccess }}</p>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="changingPassword"
|
||||
class="flex-1 px-4 py-2 rounded-lg bg-orange-500 text-white font-medium hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{{ changingPassword ? 'Updating...' : 'Update Password' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="closeChangePasswordModal"
|
||||
class="px-4 py-2 rounded-lg bg-white/10 text-white font-medium hover:bg-white/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<button
|
||||
@click="handleLogout"
|
||||
@@ -72,15 +201,122 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import ControllerIndicator from '@/components/ControllerIndicator.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
|
||||
const serverName = computed(() => store.serverName)
|
||||
const version = computed(() => store.serverInfo?.version || '0.0.0')
|
||||
const serverTorAddressFromStore = computed(() => store.serverInfo?.['tor-address'] || null)
|
||||
const torAddressFromRpc = ref<string | null>(null)
|
||||
const serverTorAddress = computed(() => serverTorAddressFromStore.value || torAddressFromRpc.value)
|
||||
const userDid = computed(() => {
|
||||
try {
|
||||
return localStorage.getItem('neode_did') || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const copiedOnion = ref(false)
|
||||
const showChangePasswordModal = ref(false)
|
||||
const changingPassword = ref(false)
|
||||
const changePasswordError = ref('')
|
||||
const changePasswordSuccess = ref('')
|
||||
const changePasswordForm = ref({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
alsoChangeSsh: true,
|
||||
})
|
||||
|
||||
function validatePasswordStrength(pw: string): string | null {
|
||||
if (pw.length < 12) return 'Password must be at least 12 characters'
|
||||
if (!/[A-Z]/.test(pw)) return 'Password must contain at least one uppercase letter'
|
||||
if (!/[a-z]/.test(pw)) return 'Password must contain at least one lowercase letter'
|
||||
if (!/\d/.test(pw)) return 'Password must contain at least one digit'
|
||||
if (!/[^A-Za-z0-9]/.test(pw)) return 'Password must contain at least one special character (!@#$%^&* etc.)'
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleChangePassword() {
|
||||
changePasswordError.value = ''
|
||||
changePasswordSuccess.value = ''
|
||||
const { currentPassword, newPassword, confirmPassword, alsoChangeSsh } = changePasswordForm.value
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
changePasswordError.value = 'All fields are required'
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
changePasswordError.value = 'New passwords do not match'
|
||||
return
|
||||
}
|
||||
const strengthError = validatePasswordStrength(newPassword)
|
||||
if (strengthError) {
|
||||
changePasswordError.value = strengthError
|
||||
return
|
||||
}
|
||||
changingPassword.value = true
|
||||
try {
|
||||
await rpcClient.changePassword({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
alsoChangeSsh,
|
||||
})
|
||||
changePasswordSuccess.value = 'Password updated successfully. Use the new password for login and SSH.'
|
||||
changePasswordForm.value = { currentPassword: '', newPassword: '', confirmPassword: '', alsoChangeSsh: true }
|
||||
setTimeout(() => {
|
||||
closeChangePasswordModal()
|
||||
}, 2000)
|
||||
} catch (e) {
|
||||
changePasswordError.value = e instanceof Error ? e.message : 'Failed to change password'
|
||||
} finally {
|
||||
changingPassword.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function copyOnionAddress() {
|
||||
const addr = serverTorAddress.value
|
||||
if (!addr) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(addr)
|
||||
copiedOnion.value = true
|
||||
setTimeout(() => { copiedOnion.value = false }, 2000)
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = addr
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
copiedOnion.value = true
|
||||
setTimeout(() => { copiedOnion.value = false }, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
function closeChangePasswordModal() {
|
||||
showChangePasswordModal.value = false
|
||||
changePasswordError.value = ''
|
||||
changePasswordSuccess.value = ''
|
||||
changePasswordForm.value = { currentPassword: '', newPassword: '', confirmPassword: '', alsoChangeSsh: true }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!serverTorAddressFromStore.value) {
|
||||
try {
|
||||
const res = await rpcClient.getTorAddress()
|
||||
torAddressFromRpc.value = res.tor_address ?? null
|
||||
} catch {
|
||||
// Ignore - tor address may not be available yet
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function handleLogout() {
|
||||
await store.logout()
|
||||
|
||||
Reference in New Issue
Block a user