adding some buttons
This commit is contained in:
parent
2da8e2c698
commit
7b69200244
@ -245,6 +245,7 @@ impl RpcHandler {
|
||||
"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.set-password" => self.handle_openwrt_set_password(params).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,
|
||||
|
||||
@ -172,6 +172,59 @@ impl RpcHandler {
|
||||
Ok(serde_json::json!({ "ok": true }))
|
||||
}
|
||||
|
||||
/// Set the router's SSH login password from the app — no manual SSH/
|
||||
/// console session on the router required. Works for a fresh flash
|
||||
/// (root has no password yet — connect with `current_password: ""`)
|
||||
/// or to rotate an existing one. On success the new credentials are
|
||||
/// persisted the same way a normal login does, so the app is fully
|
||||
/// connected afterward with no separate "Connect" step needed.
|
||||
///
|
||||
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root",
|
||||
/// "current_password": "", "new_password": "..." }`
|
||||
pub(super) async fn handle_openwrt_set_password(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let p = params.unwrap_or_default();
|
||||
let host = p
|
||||
.get("host")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("host is required"))?
|
||||
.to_string();
|
||||
let ssh_user = p
|
||||
.get("ssh_user")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("root")
|
||||
.to_string();
|
||||
let current_password = p
|
||||
.get("current_password")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let new_password = p
|
||||
.get("new_password")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| anyhow::anyhow!("new_password is required"))?
|
||||
.to_string();
|
||||
|
||||
let router = Router::connect_password(&host, 22, &ssh_user, ¤t_password)?;
|
||||
router.verify_openwrt()?;
|
||||
router.set_password(&ssh_user, &new_password)?;
|
||||
|
||||
net_router::configure_router(
|
||||
&self.config.data_dir,
|
||||
net_router::RouterType::OpenWrt,
|
||||
&host,
|
||||
None,
|
||||
Some(&ssh_user),
|
||||
Some(&new_password),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(serde_json::json!({ "ok": true, "host": host }))
|
||||
}
|
||||
|
||||
/// Provision TollGate on an OpenWrt router and create the "archipelago" SSID.
|
||||
///
|
||||
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "",
|
||||
|
||||
@ -82,6 +82,22 @@ impl Router {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Set `user`'s login password via BusyBox `passwd`, non-interactively.
|
||||
///
|
||||
/// BusyBox's passwd applet reads the new password twice from stdin even
|
||||
/// without a tty (unlike shadow-utils' passwd, which requires one), so
|
||||
/// piping two lines in works. Root can always set its own (or another
|
||||
/// user's) password without supplying the old one first. This is what
|
||||
/// lets a fresh-flash router (root has no password yet) be fully set up
|
||||
/// from the app — no manual SSH/console session required.
|
||||
pub fn set_password(&self, user: &str, new_password: &str) -> Result<()> {
|
||||
let q = crate::uci::shell_quote(new_password);
|
||||
let uq = crate::uci::shell_quote(user);
|
||||
let cmd = format!("printf '%s\\n%s\\n' {q} {q} | passwd {uq} 2>&1");
|
||||
self.run_ok(&cmd)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the remote device is actually running OpenWrt.
|
||||
pub fn verify_openwrt(&self) -> Result<String> {
|
||||
let release = self
|
||||
|
||||
@ -60,6 +60,6 @@ impl Router {
|
||||
}
|
||||
|
||||
/// Wrap a value in single quotes, escaping any embedded single quotes.
|
||||
fn shell_quote(s: &str) -> String {
|
||||
pub(crate) fn shell_quote(s: &str) -> String {
|
||||
format!("'{}'", s.replace('\'', r"'\''"))
|
||||
}
|
||||
|
||||
25
neode-ui/package-lock.json
generated
25
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.115-alpha",
|
||||
"version": "1.7.116-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.115-alpha",
|
||||
"version": "1.7.116-alpha",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
@ -150,7 +150,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@ -1812,7 +1811,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -1836,7 +1834,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@ -3923,7 +3920,6 @@
|
||||
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
@ -3973,7 +3969,6 @@
|
||||
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"colorette": "^2.0.20",
|
||||
@ -4474,7 +4469,6 @@
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@ -4960,7 +4954,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@ -5979,7 +5972,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -8172,7 +8164,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@ -8222,7 +8213,6 @@
|
||||
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.1.0",
|
||||
"data-urls": "^5.0.0",
|
||||
@ -8325,8 +8315,7 @@
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
@ -9082,7 +9071,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -9744,7 +9732,6 @@
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@ -11000,7 +10987,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -11256,7 +11242,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -11498,7 +11483,6 @@
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@ -11661,7 +11645,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -11675,7 +11658,6 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@ -11768,7 +11750,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
||||
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.30",
|
||||
"@vue/compiler-sfc": "3.5.30",
|
||||
|
||||
@ -87,6 +87,15 @@ const detecting = ref(false)
|
||||
const detectError = ref('')
|
||||
const detectedCandidates = ref<string[]>([])
|
||||
|
||||
// Set/change router password — lets a fresh-flash router (root has no
|
||||
// password yet) get fully connected without the user ever opening a
|
||||
// terminal or LuCI themselves.
|
||||
const showSetPassword = ref(false)
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const settingPassword = ref(false)
|
||||
const setPasswordError = ref('')
|
||||
|
||||
const provisioning = ref(false)
|
||||
const provisionError = ref('')
|
||||
const provisionSuccess = ref(false)
|
||||
@ -148,6 +157,39 @@ async function connect() {
|
||||
}
|
||||
}
|
||||
|
||||
async function setPasswordAndConnect() {
|
||||
if (!host.value.trim() || !newPassword.value) return
|
||||
setPasswordError.value = ''
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
setPasswordError.value = 'Passwords do not match.'
|
||||
return
|
||||
}
|
||||
settingPassword.value = true
|
||||
try {
|
||||
await rpcClient.call({
|
||||
method: 'openwrt.set-password',
|
||||
params: {
|
||||
host: host.value.trim(),
|
||||
ssh_user: sshUser.value,
|
||||
current_password: sshPassword.value,
|
||||
new_password: newPassword.value,
|
||||
},
|
||||
timeout: 20000,
|
||||
})
|
||||
// Router accepted the new password — log in with it right away so the
|
||||
// user never has to separately "Connect" after this.
|
||||
sshPassword.value = newPassword.value
|
||||
showSetPassword.value = false
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
await connect()
|
||||
} catch (e) {
|
||||
setPasswordError.value = e instanceof Error ? e.message : String(e)
|
||||
} finally {
|
||||
settingPassword.value = false
|
||||
}
|
||||
}
|
||||
|
||||
interface WiredInterface { name: string; type: string; state: string; ipv4: string[] }
|
||||
|
||||
async function detectRouter() {
|
||||
@ -464,6 +506,45 @@ onMounted(() => load())
|
||||
>
|
||||
{{ connecting ? 'Connecting…' : 'Connect' }}
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-xs text-white/40 hover:text-white/70 transition-colors text-center"
|
||||
@click="showSetPassword = !showSetPassword"
|
||||
>
|
||||
First login, or don't know the password? Set one →
|
||||
</button>
|
||||
<div v-if="showSetPassword" class="space-y-3 pt-2 border-t border-white/10">
|
||||
<p class="text-xs text-white/40">
|
||||
Sets the router's SSH password directly — no need to SSH in yourself first.
|
||||
Leave "Password" above blank if this is a fresh flash (no password set yet).
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-white/40 mb-1">New password</label>
|
||||
<input
|
||||
v-model="newPassword"
|
||||
type="password"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/40 mb-1">Confirm password</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
:disabled="settingPassword || !host.trim() || !newPassword"
|
||||
class="glass-button w-full text-sm font-medium"
|
||||
:class="settingPassword || !host.trim() || !newPassword ? 'opacity-40 cursor-not-allowed' : ''"
|
||||
@click="setPasswordAndConnect"
|
||||
>
|
||||
{{ settingPassword ? 'Setting password…' : 'Set Password & Connect' }}
|
||||
</button>
|
||||
<p v-if="setPasswordError" class="text-xs text-red-400">{{ setPasswordError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="error" class="mt-3 text-xs text-red-400">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user