Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/rkvm-web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@
.tb-err { color: var(--danger); max-width: 220px; overflow: hidden; text-overflow: ellipsis;
white-space: nowrap; }

.nopw-banner { position: fixed; top: 8px; left: 8px; z-index: 30; max-width: min(460px, calc(100vw - 16px));
display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px; border-radius: var(--r1);
background: rgba(120, 22, 32, 0.92); border: 1px solid var(--danger); color: #fff;
font: 500 12.5px/1.4 var(--font); box-shadow: var(--shadow); }
.nopw-banner button { flex: none; background: none; border: 0; color: #fff; cursor: pointer;
font-size: 12px; padding: 0 2px; opacity: .8; }
.nopw-banner button:hover { opacity: 1; }
.nopw-card { margin: 0 0 14px; padding: 10px 12px; max-width: 320px; border-radius: var(--r1);
background: rgba(240, 97, 109, 0.12); border: 1px solid rgba(240, 97, 109, 0.5);
color: #ffd5d9; font-size: 12.5px; }

.drawer-scrim { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px); z-index: 40; animation: fade 0.18s ease; }
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: 360px; overflow-y: auto;
Expand Down
28 changes: 26 additions & 2 deletions crates/rkvm-web/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub fn App() -> impl IntoView {
let checking = RwSignal::new(true);
let is_setup = RwSignal::new(true);
let needs_login = RwSignal::new(false);
let no_password = RwSignal::new(false);
let nopw_dismissed = RwSignal::new(false);
let conn_state = RwSignal::new("idle".to_string());
let error = RwSignal::new(Option::<String>::None);
let password = RwSignal::new(String::new());
Expand Down Expand Up @@ -62,6 +64,7 @@ pub fn App() -> impl IntoView {
is_setup.set(setup_done);
if setup_done {
needs_login.set(rest::auth_required().await);
no_password.set(rest::no_password().await);
}
checking.set(false);
});
Expand All @@ -76,6 +79,7 @@ pub fn App() -> impl IntoView {
Ok(()) => {
is_setup.set(true);
needs_login.set(rest::auth_required().await);
no_password.set(rest::no_password().await);
}
Err(e) => error.set(Some(e)),
}
Expand Down Expand Up @@ -296,7 +300,7 @@ pub fn App() -> impl IntoView {
<button on:click=send_cad title="Send Ctrl+Alt+Del">"C·A·D"</button>
<button on:click=go_fullscreen title="Fullscreen">"⛶"</button>
<button on:click=do_ocr title="Copy text from screen (OCR)">"OCR"</button>
<button on:click=move |_| show_terminal.update(|s| *s = !*s) title="Serial terminal">"›_"</button>
<button on:click=move |_| show_terminal.update(|s| *s = !*s) title="Device shell (root)">"›_"</button>
<button on:click=move |_| show_settings.update(|s| *s = !*s) title="Settings">"⚙"</button>

<span class="tb-sep"></span>
Expand All @@ -305,7 +309,22 @@ pub fn App() -> impl IntoView {
<span class="tb-err">{move || error.get().unwrap_or_default()}</span>
</div>

<crate::settings::Settings open=show_settings rpc=rpc_store peer=peer_store />
<Show when=move || no_password.get() && !nopw_dismissed.get()>
<div class="nopw-banner" role="alert">
<span>
"⚠ No password set: anyone on this network can control this machine and open a root shell on the device. Set one in ⚙ Settings → Password."
</span>
<button title="Hide until reload" on:click=move |_| nopw_dismissed.set(true)>
"✕"
</button>
</div>
</Show>
<crate::settings::Settings
open=show_settings
rpc=rpc_store
peer=peer_store
no_password=no_password
/>
<crate::terminal::Terminal open=show_terminal peer=peer_store />
<Show when=move || ocr_text.get().is_some()>
<div class="panel">
Expand Down Expand Up @@ -370,6 +389,11 @@ pub fn App() -> impl IntoView {
<div class="panel">
<div class="card">
<h1>"rustkvm"</h1>
<Show when=move || no_password.get()>
<div class="nopw-card">
"No password is set. Anyone on this network can connect, control this machine and open a root shell on the device. After connecting, set a password in ⚙ Settings → Password."
</div>
</Show>
<button
on:click=do_connect
disabled=move || conn_state.get() == "connecting"
Expand Down
26 changes: 26 additions & 0 deletions crates/rkvm-web/src/net/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ pub async fn auth_required() -> bool {
}
}

/// Whether the device runs without a password (`authMode: "noPassword"`), in
/// which case anyone who can reach the web UI gets full control.
pub async fn no_password() -> bool {
match Request::get("/device").credentials(RequestCredentials::Include).send().await {
Ok(resp) if resp.ok() => resp
.json::<serde_json::Value>()
.await
.ok()
.and_then(|v| v.get("authMode").and_then(|m| m.as_str().map(|m| m == "noPassword")))
.unwrap_or(false),
_ => false,
}
}

pub async fn is_setup() -> bool {
match Request::get("/device/status").credentials(RequestCredentials::Include).send().await {
Ok(resp) => resp
Expand Down Expand Up @@ -36,6 +50,18 @@ pub async fn setup(mode: &str, password: Option<&str>) -> Result<(), String> {
if resp.ok() { Ok(()) } else { Err(format!("setup HTTP {}", resp.status())) }
}

/// Sets the first password on a device in no-password mode.
pub async fn set_password(password: &str) -> Result<(), String> {
let resp = Request::post("/auth/password-local")
.credentials(RequestCredentials::Include)
.json(&json!({ "password": password }))
.map_err(|e| format!("set-password serialize: {e}"))?
.send()
.await
.map_err(|e| format!("set-password request failed: {e}"))?;
if resp.ok() { Ok(()) } else { Err(format!("set-password HTTP {}", resp.status())) }
}

pub async fn change_password(old: &str, new: &str) -> Result<(), String> {
let resp = Request::put("/auth/password-local")
.credentials(RequestCredentials::Include)
Expand Down
32 changes: 22 additions & 10 deletions crates/rkvm-web/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub fn Settings(
open: RwSignal<bool>,
rpc: StoredValue<Option<Rc<RpcClient>>, LocalStorage>,
peer: StoredValue<Option<Rc<Peer>>, LocalStorage>,
no_password: RwSignal<bool>,
) -> impl IntoView {
let codec = RwSignal::new(String::new());
let rotation = RwSignal::new(String::new());
Expand Down Expand Up @@ -385,10 +386,17 @@ pub fn Settings(
pw_msg.set("New password too short".to_string());
return;
}
let first = no_password.get_untracked();
spawn_local(async move {
match rest::change_password(&o, &n).await {
let result = if first {
rest::set_password(&n).await
} else {
rest::change_password(&o, &n).await
};
match result {
Ok(()) => {
pw_msg.set("Password updated".to_string());
no_password.set(false);
pw_msg.set(if first { "Password set" } else { "Password updated" }.to_string());
old_pw.set(String::new());
new_pw.set(String::new());
}
Expand Down Expand Up @@ -470,20 +478,24 @@ pub fn Settings(
</section>

<section>
<h3>"Change password"</h3>
<input
type="password"
placeholder="Current password"
prop:value=move || old_pw.get()
on:input=move |ev| old_pw.set(event_target_value(&ev))
/>
<h3>"Password"</h3>
<Show when=move || !no_password.get()>
<input
type="password"
placeholder="Current password"
prop:value=move || old_pw.get()
on:input=move |ev| old_pw.set(event_target_value(&ev))
/>
</Show>
<input
type="password"
placeholder="New password"
prop:value=move || new_pw.get()
on:input=move |ev| new_pw.set(event_target_value(&ev))
/>
<button on:click=submit_password>"Update password"</button>
<button on:click=submit_password>
{move || if no_password.get() { "Set password" } else { "Update password" }}
</button>
<div class="muted">{move || pw_msg.get()}</div>
</section>

Expand Down
2 changes: 1 addition & 1 deletion crates/rkvm-web/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ pub fn Terminal(
<Show when=move || open.get()>
<div class="term">
<header>
<span>"Serial / Terminal"</span>
<span>"Device shell (root)"</span>
<button title="Close" on:click=move |_| open.set(false)>
"✕"
</button>
Expand Down
Loading