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.
|
- [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)
|
### 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<{
|
async verifyNostrRevoked(): Promise<{
|
||||||
revoked: boolean
|
revoked: boolean
|
||||||
nostr_pubkey: string
|
nostr_pubkey: string
|
||||||
|
|||||||
@ -1717,3 +1717,43 @@ html:has(body.video-background-active)::before {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-align: right;
|
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>
|
||||||
</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 -->
|
<!-- Webhook Notifications Section -->
|
||||||
<div class="glass-card px-6 py-6 mb-6">
|
<div class="glass-card px-6 py-6 mb-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@ -1120,6 +1166,7 @@ onMounted(async () => {
|
|||||||
checkClaudeStatus()
|
checkClaudeStatus()
|
||||||
loadTotpStatus()
|
loadTotpStatus()
|
||||||
loadBackups()
|
loadBackups()
|
||||||
|
loadTorServices()
|
||||||
loadWebhookConfig()
|
loadWebhookConfig()
|
||||||
if (!serverTorAddressFromStore.value) {
|
if (!serverTorAddressFromStore.value) {
|
||||||
try {
|
try {
|
||||||
@ -1156,6 +1203,17 @@ const restorePassphrase = ref('')
|
|||||||
const restoringBackup = ref(false)
|
const restoringBackup = ref(false)
|
||||||
const verifyingBackupId = ref<string | null>(null)
|
const verifyingBackupId = ref<string | null>(null)
|
||||||
const deletingBackupId = 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 backupStatusMsg = ref('')
|
||||||
const backupStatusType = ref<'success' | 'error'>('success')
|
const backupStatusType = ref<'success' | 'error'>('success')
|
||||||
|
|
||||||
@ -1172,6 +1230,43 @@ function showBackupStatus(msg: string, type: 'success' | 'error') {
|
|||||||
setTimeout(() => { backupStatusMsg.value = '' }, 5000)
|
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() {
|
async function loadBackups() {
|
||||||
loadingBackups.value = true
|
loadingBackups.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user