17aa5d7200
This commit enables serving audio files via HTTP Range requests. This is crucial for allowing HTML5 audio players to seek to specific positions within an audio file without re-downloading the entire file. The changes include: - Modifying `handle_audio` in `server/src/routes/web.rs` to parse `Range` headers. - Implementing `serve_range` to handle partial content responses. - Adding a `parse_range` helper function. - Updating tests to verify range request functionality. - Adding `ACCEPT_RANGES: bytes` header to indicate support for range requests. - Storing recording duration in a sidecar file for faster UI rendering. - Enhancing the HTML template to support a custom audio player with seeking.
120 lines
3.8 KiB
Rust
120 lines
3.8 KiB
Rust
//! Client-side helper: ask the server for a one-time magic-link token and
|
|
//! build the URL the system browser should open.
|
|
//!
|
|
//! Kept separate from `app.rs` so the network call can be unit-tested
|
|
//! against a wiremock server without involving `webbrowser::open` (which
|
|
//! would launch a real browser during tests).
|
|
//!
|
|
//! # Behaviour on error
|
|
//!
|
|
//! Caller decides. The desktop app's `on_open_web` falls back to the
|
|
//! plain case URL if this fails — the doctor lands on the login page and
|
|
//! signs in manually rather than seeing a dead button.
|
|
|
|
use doctate_common::API_KEY_HEADER;
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
|
|
#[derive(Serialize)]
|
|
struct CreateRequest<'a> {
|
|
return_to: &'a str,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CreateResponse {
|
|
token: String,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum MagicLinkError {
|
|
#[error("http error: {0}")]
|
|
Http(#[from] reqwest::Error),
|
|
#[error("server returned status {0}")]
|
|
Status(reqwest::StatusCode),
|
|
}
|
|
|
|
/// Request a magic-link token and build the full URL to open. The
|
|
/// `return_to` path is what the browser lands on **after** the token
|
|
/// is consumed; must start with `/web/` (server enforces this too).
|
|
pub async fn build_magic_url(
|
|
http: &reqwest::Client,
|
|
server_url: &str,
|
|
api_key: &str,
|
|
return_to: &str,
|
|
) -> Result<String, MagicLinkError> {
|
|
let base = server_url.trim_end_matches('/');
|
|
let resp = http
|
|
.post(format!("{base}/api/auth/magic-link"))
|
|
.header(API_KEY_HEADER, api_key)
|
|
.json(&CreateRequest { return_to })
|
|
.send()
|
|
.await?;
|
|
|
|
if !resp.status().is_success() {
|
|
return Err(MagicLinkError::Status(resp.status()));
|
|
}
|
|
|
|
let parsed: CreateResponse = resp.json().await?;
|
|
Ok(format!("{base}/web/magic?token={}", parsed.token))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
use wiremock::matchers::{header, method, path};
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
|
|
#[tokio::test]
|
|
async fn returns_full_url_with_token() {
|
|
let server = MockServer::start().await;
|
|
let token = "tok-1234567890";
|
|
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/auth/magic-link"))
|
|
.and(header(API_KEY_HEADER, "test-key"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "token": token })))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let http = reqwest::Client::new();
|
|
let url = build_magic_url(&http, &server.uri(), "test-key", "/web/cases/abc")
|
|
.await
|
|
.expect("magic url");
|
|
assert_eq!(url, format!("{}/web/magic?token={token}", server.uri()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn propagates_server_error() {
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/auth/magic-link"))
|
|
.respond_with(ResponseTemplate::new(401))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let http = reqwest::Client::new();
|
|
let err = build_magic_url(&http, &server.uri(), "wrong-key", "/web/cases/abc")
|
|
.await
|
|
.expect_err("should error");
|
|
assert!(matches!(err, MagicLinkError::Status(s) if s.as_u16() == 401));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn trims_trailing_slash_in_server_url() {
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/auth/magic-link"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "token": "abc" })))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let http = reqwest::Client::new();
|
|
let url_with_slash = format!("{}/", server.uri());
|
|
let url = build_magic_url(&http, &url_with_slash, "k", "/web/cases/abc")
|
|
.await
|
|
.expect("magic url");
|
|
assert!(!url.contains("//web/"), "double slash leaked: {url}");
|
|
}
|
|
}
|