Files
doctate/client-desktop/src/magic_link.rs
T
Brummel 17aa5d7200 Implement audio range requests
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.
2026-04-19 22:26:04 +02:00

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}");
}
}