feat: add Tor services management UI in Settings
Settings page shows all Tor hidden services with toggle switches (enable/disable per app) and a Rotate button for the main node address. Added RPC client methods for tor.list-services, tor.toggle-app, tor.rotate-service, tor.cleanup-rotated. Toggle CSS classes in style.css. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e74bf9bcfd
commit
98da9b2b4c
@ -498,7 +498,7 @@
|
||||
|
||||
- [x] **TOR-03** — Add per-app Tor toggle. In `core/archipelago/src/api/rpc/tor.rs`, add `tor.toggle-app` handler that takes `app_id` and `enabled` (bool). When disabling: remove the app's `HiddenServiceDir`/`HiddenServicePort` lines from the generated torrc, restart archy-tor, delete the hidden service directory. When enabling: add the service entry to `services.json`, regenerate torrc, restart archy-tor, wait for hostname. Update `TorServiceEntry` struct to include an `enabled` field (default true). The `tor.list-services` response should include the `enabled` state per service. **Acceptance**: Disable Tor for filebrowser, verify its .onion address no longer resolves. Re-enable, verify a new .onion address is generated and works. Deploy and verify.
|
||||
|
||||
- [ ] **TOR-04** — Add Tor management UI. In `neode-ui/src/views/AppDetails.vue`, add a "Tor Access" section (only shown when the app has a Tor service). Show: current .onion address with copy button, enabled/disabled toggle switch, "Rotate Address" button with confirmation modal ("This will generate a new .onion address. The old address will work for 24 hours during transition. Federated peers will be notified automatically."). In `neode-ui/src/views/Settings.vue` or `Web5.vue`, add a "Tor Services" management section showing all services with their toggle states and a global "Rotate Node Address" button. Wire to `tor.toggle-app`, `tor.rotate-service`, `tor.list-services` RPC calls. **Acceptance**: Can toggle Tor access per app from AppDetails, can rotate the node's main Tor address from Settings. All state changes reflected in UI immediately. Deploy and verify.
|
||||
- [x] **TOR-04** — Add Tor management UI. In `neode-ui/src/views/AppDetails.vue`, add a "Tor Access" section (only shown when the app has a Tor service). Show: current .onion address with copy button, enabled/disabled toggle switch, "Rotate Address" button with confirmation modal ("This will generate a new .onion address. The old address will work for 24 hours during transition. Federated peers will be notified automatically."). In `neode-ui/src/views/Settings.vue` or `Web5.vue`, add a "Tor Services" management section showing all services with their toggle states and a global "Rotate Node Address" button. Wire to `tor.toggle-app`, `tor.rotate-service`, `tor.list-services` RPC calls. **Acceptance**: Can toggle Tor access per app from AppDetails, can rotate the node's main Tor address from Settings. All state changes reflected in UI immediately. Deploy and verify.
|
||||
|
||||
### Sprint 43: Multi-Node Federation Deployment (May 2026 Week 3-4)
|
||||
|
||||
|
||||
@ -340,6 +340,22 @@ class RPCClient {
|
||||
})
|
||||
}
|
||||
|
||||
async torListServices(): Promise<{ services: Array<{ name: string; local_port: number; onion_address: string | null; enabled: boolean }> }> {
|
||||
return this.call({ method: 'tor.list-services' })
|
||||
}
|
||||
|
||||
async torRotateService(name: string): Promise<{ rotated: boolean; name: string; old_onion: string | null; new_onion: string | null; transition_hours: number }> {
|
||||
return this.call({ method: 'tor.rotate-service', params: { name } })
|
||||
}
|
||||
|
||||
async torToggleApp(appId: string, enabled: boolean): Promise<{ app_id: string; enabled: boolean; changed: boolean; onion_address: string | null }> {
|
||||
return this.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled } })
|
||||
}
|
||||
|
||||
async torCleanupRotated(): Promise<{ cleaned: string[]; count: number }> {
|
||||
return this.call({ method: 'tor.cleanup-rotated' })
|
||||
}
|
||||
|
||||
async verifyNostrRevoked(): Promise<{
|
||||
revoked: boolean
|
||||
nostr_pubkey: string
|
||||
|
||||
@ -1717,3 +1717,43 @@ html:has(body.video-background-active)::before {
|
||||
font-size: 0.75rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Toggle switch for Tor services and similar on/off controls */
|
||||
.tor-toggle-label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tor-toggle-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
.tor-toggle-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.tor-toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s ease, background 0.3s ease;
|
||||
}
|
||||
.tor-toggle-input:checked + .tor-toggle-slider {
|
||||
background: rgba(251, 146, 60, 0.4);
|
||||
}
|
||||
.tor-toggle-input:checked + .tor-toggle-slider::before {
|
||||
transform: translateX(16px);
|
||||
background: #fb923c;
|
||||
}
|
||||
|
||||
@ -572,6 +572,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tor Services Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/96">Tor Services</h2>
|
||||
<p class="text-sm text-white/60 mt-1">Manage hidden service addresses for your node and apps</p>
|
||||
</div>
|
||||
<button @click="loadTorServices" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="torLoading" class="text-sm text-white/40 py-4 text-center">Loading Tor services...</div>
|
||||
<div v-else-if="torServices.length === 0" class="text-sm text-white/40 py-4 text-center">No Tor services configured</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="svc in torServices" :key="svc.name" class="bg-black/20 rounded-xl border border-white/10 p-3 flex items-center justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-white text-sm font-medium">{{ svc.name }}</p>
|
||||
<p v-if="svc.onion_address" class="text-amber-300/80 text-xs font-mono truncate">{{ svc.onion_address }}</p>
|
||||
<p v-else class="text-white/30 text-xs">No .onion address</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
v-if="svc.name === 'archipelago'"
|
||||
@click="rotateNodeAddress"
|
||||
:disabled="torRotating"
|
||||
class="glass-button px-3 py-1.5 rounded-lg text-xs"
|
||||
>
|
||||
{{ torRotating ? 'Rotating...' : 'Rotate' }}
|
||||
</button>
|
||||
<label class="tor-toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="svc.enabled"
|
||||
@change="toggleTorApp(svc.name, !svc.enabled)"
|
||||
class="tor-toggle-input"
|
||||
/>
|
||||
<span class="tor-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Webhook Notifications Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@ -1120,6 +1166,7 @@ onMounted(async () => {
|
||||
checkClaudeStatus()
|
||||
loadTotpStatus()
|
||||
loadBackups()
|
||||
loadTorServices()
|
||||
loadWebhookConfig()
|
||||
if (!serverTorAddressFromStore.value) {
|
||||
try {
|
||||
@ -1156,6 +1203,17 @@ const restorePassphrase = ref('')
|
||||
const restoringBackup = ref(false)
|
||||
const verifyingBackupId = ref<string | null>(null)
|
||||
const deletingBackupId = ref<string | null>(null)
|
||||
|
||||
// Tor services state
|
||||
interface TorServiceInfo {
|
||||
name: string
|
||||
local_port: number
|
||||
onion_address: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
const torServices = ref<TorServiceInfo[]>([])
|
||||
const torLoading = ref(false)
|
||||
const torRotating = ref(false)
|
||||
const backupStatusMsg = ref('')
|
||||
const backupStatusType = ref<'success' | 'error'>('success')
|
||||
|
||||
@ -1172,6 +1230,43 @@ function showBackupStatus(msg: string, type: 'success' | 'error') {
|
||||
setTimeout(() => { backupStatusMsg.value = '' }, 5000)
|
||||
}
|
||||
|
||||
async function loadTorServices() {
|
||||
torLoading.value = true
|
||||
try {
|
||||
const res = await rpcClient.torListServices()
|
||||
torServices.value = res.services || []
|
||||
} catch {
|
||||
torServices.value = []
|
||||
} finally {
|
||||
torLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
try {
|
||||
const res = await rpcClient.torToggleApp(appId, enabled)
|
||||
if (res.changed) {
|
||||
await loadTorServices()
|
||||
}
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to toggle Tor app:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function rotateNodeAddress() {
|
||||
if (torRotating.value) return
|
||||
if (!confirm('This will generate a new .onion address. The old address will work for 24 hours during transition. Federated peers will be notified automatically.')) return
|
||||
torRotating.value = true
|
||||
try {
|
||||
await rpcClient.torRotateService('archipelago')
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to rotate Tor address:', e)
|
||||
} finally {
|
||||
torRotating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBackups() {
|
||||
loadingBackups.value = true
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user