feat(openwrt): add "Forget router" to actually clear saved config

"Switch router" only reset local Vue state, leaving router_config.json
on disk — a reload silently reconnected with the old saved SSH
password. Add openwrt.forget RPC + forget_router_config() to delete
the saved config, and a "Forget router" button in OpenWrtGateway.vue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ssmithx 2026-07-27 18:14:54 -04:00
parent cebbd10127
commit 2da8e2c698
4 changed files with 54 additions and 0 deletions

View File

@ -244,6 +244,7 @@ impl RpcHandler {
// OpenWrt / TollGate
"openwrt.scan" => self.handle_openwrt_scan(params).await,
"openwrt.get-status" => self.handle_openwrt_get_status(params).await,
"openwrt.forget" => self.handle_openwrt_forget().await,
"openwrt.provision-tollgate" => self.handle_openwrt_provision_tollgate(params).await,
"openwrt.scan-wifi" => self.handle_openwrt_scan_wifi(params).await,
"openwrt.configure-wan" => self.handle_openwrt_configure_wan(params).await,

View File

@ -165,6 +165,13 @@ impl RpcHandler {
}))
}
/// Forget the saved router config — deletes `router_config.json` so the
/// app requires a fresh login next time. Takes no params.
pub(super) async fn handle_openwrt_forget(&self) -> Result<serde_json::Value> {
net_router::forget_router_config(&self.config.data_dir).await?;
Ok(serde_json::json!({ "ok": true }))
}
/// Provision TollGate on an OpenWrt router and create the "archipelago" SSID.
///
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "",

View File

@ -366,6 +366,17 @@ pub async fn save_router_config(data_dir: &Path, config: &RouterConfig) -> Resul
.context("Writing router config")
}
/// Remove the saved router config (credentials + address) so the app forgets
/// this router entirely and the next call requires a fresh login.
pub async fn forget_router_config(data_dir: &Path) -> Result<()> {
let path = data_dir.join(ROUTER_CONFIG_FILE);
match fs::remove_file(&path).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e).context("Removing router config"),
}
}
/// Validate that an IP string is a private/LAN address (not public, not localhost).
fn is_valid_private_ip(ip_str: &str) -> bool {
let ip: std::net::IpAddr = match ip_str.parse() {

View File

@ -203,6 +203,33 @@ function disconnectRouter() {
showConnectForm.value = true
}
const forgetting = ref(false)
const forgetError = ref('')
// Unlike disconnectRouter() (which only resets local state, leaving the saved
// router_config.json in place so onMounted reconnects on next visit), this
// deletes the saved credentials server-side so the router is truly forgotten.
async function forgetRouter() {
if (!confirm('Forget this router? You will need to log in again next time.')) return
forgetting.value = true
forgetError.value = ''
try {
await rpcClient.call({ method: 'openwrt.forget', timeout: 10000 })
status.value = null
connectedParams.value = null
host.value = ''
sshUser.value = 'root'
sshPassword.value = ''
detectError.value = ''
detectedCandidates.value = []
showConnectForm.value = true
} catch (e) {
forgetError.value = e instanceof Error ? e.message : String(e)
} finally {
forgetting.value = false
}
}
async function provisionTollgate() {
provisioning.value = true
provisionError.value = ''
@ -499,7 +526,15 @@ onMounted(() => load())
<button class="text-xs text-white/40 hover:text-white/70 transition-colors" @click="disconnectRouter">
Switch router
</button>
<button
class="text-xs text-red-400/70 hover:text-red-400 transition-colors"
:disabled="forgetting"
@click="forgetRouter"
>
{{ forgetting ? 'Forgetting…' : 'Forget router' }}
</button>
</div>
<p v-if="forgetError" class="mt-2 text-xs text-red-400">{{ forgetError }}</p>
</div>
<!-- WAN / Uplink -->