feat(trx-client): add runtime multi-rig discovery and switching

This commit is contained in:
2026-02-25 22:39:02 +01:00
parent 30ad6d1bb4
commit 6f836a93e9
8 changed files with 365 additions and 40 deletions
+5 -1
View File
@@ -189,6 +189,9 @@ async fn async_init() -> DynResult<AppState> {
let remote_token = cli.token.clone().or_else(|| cfg.remote.auth.token.clone());
let remote_rig_id = cli.rig_id.clone().or_else(|| cfg.remote.rig_id.clone());
if let Ok(mut guard) = frontend_runtime.remote_active_rig_id.lock() {
*guard = remote_rig_id.clone();
}
let poll_interval_ms = cli.poll_interval_ms.unwrap_or(cfg.remote.poll_interval_ms);
@@ -254,7 +257,8 @@ async fn async_init() -> DynResult<AppState> {
let remote_cfg = RemoteClientConfig {
addr: remote_endpoint.connect_addr(),
token: remote_token,
rig_id: remote_rig_id,
selected_rig_id: frontend_runtime.remote_active_rig_id.clone(),
known_rigs: frontend_runtime.remote_rigs.clone(),
poll_interval: Duration::from_millis(poll_interval_ms),
};
let remote_shutdown_rx = shutdown_rx.clone();
+111 -8
View File
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: BSD-2-Clause
use std::time::Duration;
use std::{sync::Arc, sync::Mutex};
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
@@ -13,7 +14,9 @@ use tracing::{info, warn};
use trx_core::rig::request::RigRequest;
use trx_core::rig::state::RigState;
use trx_core::{RigError, RigResult};
use trx_frontend::RemoteRigEntry;
use trx_protocol::rig_command_to_client;
use trx_protocol::types::RigEntry;
use trx_protocol::{ClientCommand, ClientEnvelope, ClientResponse};
const DEFAULT_REMOTE_PORT: u16 = 4530;
@@ -40,7 +43,8 @@ impl RemoteEndpoint {
pub struct RemoteClientConfig {
pub addr: String,
pub token: Option<String>,
pub rig_id: Option<String>,
pub selected_rig_id: Arc<Mutex<Option<String>>>,
pub known_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
pub poll_interval: Duration,
}
@@ -118,7 +122,9 @@ async fn handle_connection(
continue;
}
last_poll = Instant::now();
if let Err(e) = send_command(config, &mut writer, &mut reader, ClientCommand::GetState, state_tx).await {
if let Err(e) =
refresh_remote_snapshot(config, &mut writer, &mut reader, state_tx).await
{
warn!("Remote poll failed: {}", e);
}
}
@@ -187,11 +193,100 @@ async fn send_command(
fn build_envelope(config: &RemoteClientConfig, cmd: ClientCommand) -> ClientEnvelope {
ClientEnvelope {
token: config.token.clone(),
rig_id: config.rig_id.clone(),
rig_id: selected_rig_id(config),
cmd,
}
}
async fn refresh_remote_snapshot(
config: &RemoteClientConfig,
writer: &mut tokio::net::tcp::OwnedWriteHalf,
reader: &mut BufReader<tokio::net::tcp::OwnedReadHalf>,
state_tx: &watch::Sender<RigState>,
) -> RigResult<()> {
let rigs = send_get_rigs(config, writer, reader).await?;
cache_remote_rigs(config, &rigs);
if rigs.is_empty() {
return Err(RigError::communication("GetRigs returned no rigs"));
}
let selected = selected_rig_id(config);
let fallback = &rigs[0];
let target = selected
.as_deref()
.and_then(|id| rigs.iter().find(|entry| entry.rig_id == id))
.unwrap_or(fallback);
if selected.as_deref() != Some(target.rig_id.as_str()) {
set_selected_rig_id(config, Some(target.rig_id.clone()));
}
let _ = state_tx.send(RigState::from_snapshot(target.state.clone()));
Ok(())
}
async fn send_get_rigs(
config: &RemoteClientConfig,
writer: &mut tokio::net::tcp::OwnedWriteHalf,
reader: &mut BufReader<tokio::net::tcp::OwnedReadHalf>,
) -> RigResult<Vec<RigEntry>> {
let envelope = build_envelope(config, ClientCommand::GetRigs);
let payload = serde_json::to_string(&envelope)
.map_err(|e| RigError::communication(format!("JSON serialize failed: {e}")))?;
time::timeout(
IO_TIMEOUT,
writer.write_all(format!("{}\n", payload).as_bytes()),
)
.await
.map_err(|_| RigError::communication(format!("write timed out after {:?}", IO_TIMEOUT)))?
.map_err(|e| RigError::communication(format!("write failed: {e}")))?;
time::timeout(IO_TIMEOUT, writer.flush())
.await
.map_err(|_| RigError::communication(format!("flush timed out after {:?}", IO_TIMEOUT)))?
.map_err(|e| RigError::communication(format!("flush failed: {e}")))?;
let line = time::timeout(IO_TIMEOUT, read_limited_line(reader, MAX_JSON_LINE_BYTES))
.await
.map_err(|_| RigError::communication(format!("read timed out after {:?}", IO_TIMEOUT)))?
.map_err(|e| RigError::communication(format!("read failed: {e}")))?;
let line = line.ok_or_else(|| RigError::communication("connection closed by remote"))?;
let resp: ClientResponse = serde_json::from_str(line.trim_end())
.map_err(|e| RigError::communication(format!("invalid response: {e}")))?;
if resp.success {
return resp
.rigs
.ok_or_else(|| RigError::communication("missing rigs list in GetRigs response"));
}
Err(RigError::communication(
resp.error.unwrap_or_else(|| "remote error".into()),
))
}
fn cache_remote_rigs(config: &RemoteClientConfig, rigs: &[RigEntry]) {
if let Ok(mut guard) = config.known_rigs.lock() {
*guard = rigs
.iter()
.map(|entry| RemoteRigEntry {
rig_id: entry.rig_id.clone(),
state: entry.state.clone(),
})
.collect();
}
}
fn selected_rig_id(config: &RemoteClientConfig) -> Option<String> {
config.selected_rig_id.lock().ok().and_then(|g| g.clone())
}
fn set_selected_rig_id(config: &RemoteClientConfig, value: Option<String>) {
if let Ok(mut guard) = config.selected_rig_id.lock() {
*guard = value;
}
}
async fn read_limited_line<R: AsyncBufRead + Unpin>(
reader: &mut R,
max_bytes: usize,
@@ -316,6 +411,7 @@ fn parse_port(port_str: &str) -> Result<u16, String> {
#[cfg(test)]
mod tests {
use super::{parse_remote_url, RemoteClientConfig, RemoteEndpoint};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
@@ -326,6 +422,7 @@ mod tests {
use trx_core::rig::state::RigSnapshot;
use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo, RigStatus, RigTxStatus};
use trx_core::{RigMode, RigState};
use trx_protocol::types::RigEntry;
use trx_protocol::ClientResponse;
#[test]
@@ -441,11 +538,15 @@ mod tests {
async fn reconnects_and_updates_state_after_drop() {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local addr");
let snapshot = sample_snapshot();
let response = serde_json::to_string(&ClientResponse {
success: true,
rig_id: None,
state: Some(sample_snapshot()),
rigs: None,
rig_id: Some("server".to_string()),
state: None,
rigs: Some(vec![RigEntry {
rig_id: "default".to_string(),
state: snapshot.clone(),
}]),
error: None,
})
.expect("serialize response")
@@ -481,7 +582,8 @@ mod tests {
RemoteClientConfig {
addr: addr.to_string(),
token: None,
rig_id: None,
selected_rig_id: Arc::new(Mutex::new(None)),
known_rigs: Arc::new(Mutex::new(Vec::new())),
poll_interval: Duration::from_millis(100),
},
req_rx,
@@ -515,7 +617,8 @@ mod tests {
let config = RemoteClientConfig {
addr: "127.0.0.1:4530".to_string(),
token: Some("secret".to_string()),
rig_id: Some("sdr".to_string()),
selected_rig_id: Arc::new(Mutex::new(Some("sdr".to_string()))),
known_rigs: Arc::new(Mutex::new(Vec::new())),
poll_interval: Duration::from_millis(500),
};
let envelope = super::build_envelope(&config, trx_protocol::ClientCommand::GetState);
+13
View File
@@ -14,8 +14,15 @@ use tokio::task::JoinHandle;
use trx_core::audio::AudioStreamInfo;
use trx_core::decode::{AprsPacket, CwEvent, DecodedMessage, Ft8Message, WsprMessage};
use trx_core::rig::state::RigSnapshot;
use trx_core::{DynResult, RigRequest, RigState};
#[derive(Clone, Debug)]
pub struct RemoteRigEntry {
pub rig_id: String,
pub state: RigSnapshot,
}
/// Trait implemented by concrete frontends to expose a runner entrypoint.
pub trait FrontendSpawner {
fn spawn_frontend(
@@ -140,6 +147,10 @@ pub struct FrontendRuntimeContext {
pub http_auth_cookie_secure: bool,
/// HTTP frontend auth cookie same-site policy
pub http_auth_cookie_same_site: String,
/// Currently selected remote rig id (used by remote client routing).
pub remote_active_rig_id: Arc<Mutex<Option<String>>>,
/// Cached remote rig list from GetRigs polling.
pub remote_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
}
impl FrontendRuntimeContext {
@@ -165,6 +176,8 @@ impl FrontendRuntimeContext {
http_auth_session_ttl_secs: 480 * 60,
http_auth_cookie_secure: false,
http_auth_cookie_same_site: "Lax".to_string(),
remote_active_rig_id: Arc::new(Mutex::new(None)),
remote_rigs: Arc::new(Mutex::new(Vec::new())),
}
}
}
@@ -19,6 +19,7 @@ use trx_frontend::{FrontendRuntimeContext, FrontendSpawner};
use trx_protocol::auth::{SimpleTokenValidator, TokenValidator};
use trx_protocol::codec::parse_envelope;
use trx_protocol::mapping;
use trx_protocol::types::{ClientCommand, RigEntry};
use trx_protocol::ClientResponse;
const IO_TIMEOUT: Duration = Duration::from_secs(10);
@@ -125,6 +126,30 @@ async fn handle_client(
continue;
}
if let Some(rig_id) = envelope.rig_id.as_ref() {
if let Ok(mut active) = context.remote_active_rig_id.lock() {
*active = Some(rig_id.clone());
}
}
if matches!(&envelope.cmd, ClientCommand::GetRigs) {
let resp = ClientResponse {
success: true,
rig_id: Some("client".to_string()),
state: None,
rigs: Some(snapshot_remote_rigs(context.as_ref())),
error: None,
};
send_response(&mut writer, &resp).await?;
continue;
}
let active_rig_id = context
.remote_active_rig_id
.lock()
.ok()
.and_then(|v| v.clone());
let rig_cmd = mapping::client_command_to_rig(envelope.cmd);
let (resp_tx, resp_rx) = oneshot::channel();
@@ -139,7 +164,7 @@ async fn handle_client(
error!("Failed to send request to rig_task: {:?}", e);
let resp = ClientResponse {
success: false,
rig_id: None,
rig_id: active_rig_id.clone(),
state: None,
rigs: None,
error: Some("Internal error: rig task not available".into()),
@@ -150,7 +175,7 @@ async fn handle_client(
Err(_) => {
let resp = ClientResponse {
success: false,
rig_id: None,
rig_id: active_rig_id.clone(),
state: None,
rigs: None,
error: Some("Internal error: request queue timeout".into()),
@@ -164,7 +189,7 @@ async fn handle_client(
Ok(Ok(Ok(snapshot))) => {
let resp = ClientResponse {
success: true,
rig_id: None,
rig_id: active_rig_id.clone(),
state: Some(snapshot),
rigs: None,
error: None,
@@ -174,7 +199,7 @@ async fn handle_client(
Ok(Ok(Err(err))) => {
let resp = ClientResponse {
success: false,
rig_id: None,
rig_id: active_rig_id.clone(),
state: None,
rigs: None,
error: Some(err.message),
@@ -185,7 +210,7 @@ async fn handle_client(
error!("Rig response oneshot recv error: {:?}", e);
let resp = ClientResponse {
success: false,
rig_id: None,
rig_id: active_rig_id.clone(),
state: None,
rigs: None,
error: Some("Internal error waiting for rig response".into()),
@@ -195,7 +220,7 @@ async fn handle_client(
Err(_) => {
let resp = ClientResponse {
success: false,
rig_id: None,
rig_id: active_rig_id.clone(),
state: None,
rigs: None,
error: Some("Request timed out waiting for rig response".into()),
@@ -208,6 +233,23 @@ async fn handle_client(
Ok(())
}
fn snapshot_remote_rigs(context: &FrontendRuntimeContext) -> Vec<RigEntry> {
context
.remote_rigs
.lock()
.ok()
.map(|entries| {
entries
.iter()
.map(|entry| RigEntry {
rig_id: entry.rig_id.clone(),
state: entry.state.clone(),
})
.collect()
})
.unwrap_or_default()
}
async fn read_limited_line<R: AsyncBufRead + Unpin>(
reader: &mut R,
max_bytes: usize,
@@ -277,6 +277,8 @@ const loadingTitle = document.getElementById("loading-title");
const loadingSub = document.getElementById("loading-sub");
const headerSigCanvas = document.getElementById("header-sig-canvas");
const themeToggleBtn = document.getElementById("theme-toggle");
const rigSwitchSelect = document.getElementById("rig-switch-select");
const rigSwitchBtn = document.getElementById("rig-switch-btn");
let lastControl;
let lastTxEn = null;
@@ -303,6 +305,7 @@ function vfoColor(idx) {
let jogAngle = 0;
let lastClientCount = null;
let lastLocked = false;
let lastRigIds = [];
const originalTitle = document.title;
const savedTheme = loadSetting("theme", null);
@@ -941,6 +944,33 @@ function render(update) {
if (typeof update.clients === "number") {
document.getElementById("about-clients").textContent = update.clients;
}
if (typeof update.active_rig_id === "string" && update.active_rig_id.length > 0) {
document.getElementById("about-active-rig").textContent = update.active_rig_id;
}
if (Array.isArray(update.rig_ids)) {
lastRigIds = update.rig_ids.filter((id) => typeof id === "string" && id.length > 0);
document.getElementById("about-rig-list").textContent = lastRigIds.length ? lastRigIds.join(", ") : "--";
if (rigSwitchSelect) {
const selectedBefore = rigSwitchSelect.value;
rigSwitchSelect.innerHTML = "";
lastRigIds.forEach((id) => {
const opt = document.createElement("option");
opt.value = id;
opt.textContent = id;
rigSwitchSelect.appendChild(opt);
});
const preferred = (typeof update.active_rig_id === "string" && lastRigIds.includes(update.active_rig_id))
? update.active_rig_id
: selectedBefore;
if (preferred && lastRigIds.includes(preferred)) {
rigSwitchSelect.value = preferred;
}
rigSwitchSelect.disabled = lastRigIds.length === 0;
if (rigSwitchBtn) {
rigSwitchBtn.disabled = lastRigIds.length === 0 || authRole === "rx";
}
}
}
if (typeof update.rigctl_clients === "number") {
document.getElementById("about-rigctl-clients").textContent = update.rigctl_clients;
}
@@ -1080,6 +1110,36 @@ async function postPath(path) {
return resp;
}
async function switchRig() {
if (!rigSwitchSelect || !rigSwitchSelect.value) {
showHint("No rig selected", 1500);
return;
}
if (authRole === "rx") {
showHint("Control role required", 1500);
return;
}
if (!lastRigIds.includes(rigSwitchSelect.value)) {
showHint("Unknown rig", 1500);
return;
}
if (rigSwitchBtn) rigSwitchBtn.disabled = true;
showHint("Switching rig…");
try {
await postPath(`/select_rig?rig_id=${encodeURIComponent(rigSwitchSelect.value)}`);
showHint("Rig switch requested", 1500);
} catch (err) {
showHint("Rig switch failed", 2000);
console.error(err);
} finally {
if (rigSwitchBtn) rigSwitchBtn.disabled = authRole === "rx";
}
}
if (rigSwitchBtn) {
rigSwitchBtn.addEventListener("click", switchRig);
}
powerBtn.addEventListener("click", async () => {
powerBtn.disabled = true;
showHint("Sending...");
@@ -275,6 +275,9 @@
<tr><td>Server address</td><td id="about-server-addr">--</td></tr>
<tr><td>Server callsign</td><td id="about-server-call">--</td></tr>
<tr><td>Rig</td><td id="about-rig-info">--</td></tr>
<tr><td>Active rig</td><td id="about-active-rig">--</td></tr>
<tr><td>Available rigs</td><td id="about-rig-list">--</td></tr>
<tr><td>Switch rig</td><td><select id="rig-switch-select"></select> <button id="rig-switch-btn" type="button">Switch</button></td></tr>
<tr><td>Rig connection</td><td id="about-rig-access">--</td></tr>
<tr><td>Supported modes</td><td id="about-modes">--</td></tr>
<tr><td>VFOs</td><td id="about-vfos">--</td></tr>
@@ -16,7 +16,7 @@ use tokio_stream::wrappers::{IntervalStream, WatchStream};
use trx_core::radio::freq::Freq;
use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo};
use trx_core::{RigCommand, RigRequest, RigSnapshot, RigState};
use trx_frontend::FrontendRuntimeContext;
use trx_frontend::{FrontendRuntimeContext, RemoteRigEntry};
use trx_protocol::{parse_mode, ClientResponse};
use crate::server::status;
@@ -42,6 +42,8 @@ fn inject_frontend_meta(
http_clients: usize,
rigctl_clients: usize,
rigctl_addr: Option<String>,
active_rig_id: Option<String>,
rig_ids: Vec<String>,
) -> String {
let mut value: serde_json::Value = match serde_json::from_str(json) {
Ok(v) => v,
@@ -59,6 +61,10 @@ fn inject_frontend_meta(
if let Some(addr) = rigctl_addr {
map.insert("rigctl_addr".to_string(), serde_json::json!(addr));
}
if let Some(rig_id) = active_rig_id {
map.insert("active_rig_id".to_string(), serde_json::json!(rig_id));
}
map.insert("rig_ids".to_string(), serde_json::json!(rig_ids));
serde_json::to_string(&value).unwrap_or_else(|_| json.to_string())
}
@@ -72,6 +78,23 @@ fn rigctl_addr_from_context(context: &FrontendRuntimeContext) -> Option<String>
.map(|addr| addr.to_string())
}
fn active_rig_id_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context
.remote_active_rig_id
.lock()
.ok()
.and_then(|v| v.clone())
}
fn rig_ids_from_context(context: &FrontendRuntimeContext) -> Vec<String> {
context
.remote_rigs
.lock()
.ok()
.map(|entries| entries.iter().map(|r| r.rig_id.clone()).collect())
.unwrap_or_default()
}
#[get("/events")]
pub async fn events(
state: web::Data<watch::Receiver<RigState>>,
@@ -91,6 +114,8 @@ pub async fn events(
count,
context.rigctl_clients.load(Ordering::Relaxed),
rigctl_addr_from_context(context.get_ref().as_ref()),
active_rig_id_from_context(context.get_ref().as_ref()),
rig_ids_from_context(context.get_ref().as_ref()),
);
let initial_stream =
once(async move { Ok::<Bytes, Error>(Bytes::from(format!("data: {initial_json}\n\n"))) });
@@ -108,6 +133,8 @@ pub async fn events(
counter.load(Ordering::Relaxed),
context.rigctl_clients.load(Ordering::Relaxed),
rigctl_addr_from_context(context.as_ref()),
active_rig_id_from_context(context.as_ref()),
rig_ids_from_context(context.as_ref()),
);
Ok::<Bytes, Error>(Bytes::from(format!("data: {json}\n\n")))
})
@@ -471,9 +498,90 @@ pub async fn clear_cw_decode(
send_command(&rig_tx, RigCommand::ResetCwDecoder).await
}
#[derive(serde::Serialize)]
struct RigListItem {
rig_id: String,
manufacturer: String,
model: String,
initialized: bool,
}
#[derive(serde::Serialize)]
struct RigListResponse {
active_rig_id: Option<String>,
rigs: Vec<RigListItem>,
}
fn build_rig_list_payload(context: &FrontendRuntimeContext) -> RigListResponse {
let active_rig_id = active_rig_id_from_context(context);
let rigs = context
.remote_rigs
.lock()
.ok()
.map(|entries| entries.iter().map(map_rig_entry).collect())
.unwrap_or_default();
RigListResponse {
active_rig_id,
rigs,
}
}
fn map_rig_entry(entry: &RemoteRigEntry) -> RigListItem {
RigListItem {
rig_id: entry.rig_id.clone(),
manufacturer: entry.state.info.manufacturer.clone(),
model: entry.state.info.model.clone(),
initialized: entry.state.initialized,
}
}
#[get("/rigs")]
pub async fn list_rigs(
context: web::Data<Arc<FrontendRuntimeContext>>,
) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().json(build_rig_list_payload(context.get_ref().as_ref())))
}
#[derive(serde::Deserialize)]
pub struct SelectRigQuery {
pub rig_id: String,
}
#[post("/select_rig")]
pub async fn select_rig(
query: web::Query<SelectRigQuery>,
context: web::Data<Arc<FrontendRuntimeContext>>,
) -> Result<HttpResponse, Error> {
let rig_id = query.rig_id.trim();
if rig_id.is_empty() {
return Err(actix_web::error::ErrorBadRequest(
"rig_id must not be empty",
));
}
let known = context
.remote_rigs
.lock()
.ok()
.map(|entries| entries.iter().any(|entry| entry.rig_id == rig_id))
.unwrap_or(false);
if !known {
return Err(actix_web::error::ErrorBadRequest(format!(
"unknown rig_id: {rig_id}"
)));
}
if let Ok(mut active) = context.remote_active_rig_id.lock() {
*active = Some(rig_id.to_string());
}
Ok(HttpResponse::Ok().json(build_rig_list_payload(context.get_ref().as_ref())))
}
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(index)
.service(status_api)
.service(list_rigs)
.service(events)
.service(decode_events)
.service(toggle_power)
@@ -497,6 +605,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(clear_cw_decode)
.service(clear_ft8_decode)
.service(clear_wspr_decode)
.service(select_rig)
.service(crate::server::audio::audio_ws)
.service(favicon)
.service(logo)
@@ -9,8 +9,9 @@
//! - `Control`: full access including TX/PTT control
use actix_web::{
cookie::Cookie,
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
get, post, web, Error, HttpRequest, HttpResponse, Responder, cookie::Cookie,
get, post, web, Error, HttpRequest, HttpResponse, Responder,
};
use futures_util::future::LocalBoxFuture;
use serde::{Deserialize, Serialize};
@@ -18,7 +19,6 @@ use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
/// Unique session identifier (hex-encoded 128-bit random)
pub type SessionId = String;
@@ -309,12 +309,10 @@ pub async fn login(
let ttl_secs = auth_state.config.session_ttl.as_secs() as i64;
cookie.set_max_age(actix_web::cookie::time::Duration::seconds(ttl_secs));
Ok(HttpResponse::Ok()
.cookie(cookie)
.json(LoginResponse {
authenticated: true,
role: role.as_str().to_string(),
}))
Ok(HttpResponse::Ok().cookie(cookie).json(LoginResponse {
authenticated: true,
role: role.as_str().to_string(),
}))
}
/// POST /auth/logout
@@ -338,11 +336,9 @@ pub async fn logout(
cookie.set_http_only(true);
cookie.set_max_age(actix_web::cookie::time::Duration::seconds(0));
Ok(HttpResponse::Ok()
.cookie(cookie)
.json(serde_json::json!({
"logged_out": true
})))
Ok(HttpResponse::Ok().cookie(cookie).json(serde_json::json!({
"logged_out": true
})))
}
/// GET /auth/session
@@ -424,10 +420,12 @@ impl RouteAccess {
// Read-only routes
if path == "/status"
|| path == "/rigs"
|| path == "/events"
|| path == "/decode"
|| path == "/audio"
|| path.starts_with("/status?")
|| path.starts_with("/rigs?")
|| path.starts_with("/events?")
|| path.starts_with("/decode?")
|| path.starts_with("/audio?")
@@ -498,9 +496,7 @@ where
}
// For protected routes, check auth
let auth_state = req
.app_data::<web::Data<AuthState>>()
.cloned();
let auth_state = req.app_data::<web::Data<AuthState>>().cloned();
if let Some(auth_state) = auth_state {
if !auth_state.config.enabled {
@@ -586,6 +582,7 @@ mod tests {
#[test]
fn test_route_access_read_paths() {
assert_eq!(RouteAccess::from_path("/status"), RouteAccess::Read);
assert_eq!(RouteAccess::from_path("/rigs"), RouteAccess::Read);
assert_eq!(RouteAccess::from_path("/events"), RouteAccess::Read);
assert_eq!(RouteAccess::from_path("/decode"), RouteAccess::Read);
assert_eq!(RouteAccess::from_path("/audio"), RouteAccess::Read);
@@ -593,14 +590,8 @@ mod tests {
#[test]
fn test_route_access_control_paths() {
assert_eq!(
RouteAccess::from_path("/set_freq"),
RouteAccess::Control
);
assert_eq!(
RouteAccess::from_path("/set_mode"),
RouteAccess::Control
);
assert_eq!(RouteAccess::from_path("/set_freq"), RouteAccess::Control);
assert_eq!(RouteAccess::from_path("/set_mode"), RouteAccess::Control);
}
#[test]