Skip to main content

pipedash_web/
pairing.rs

1use axum::http::StatusCode;
2use chrono::{
3    DateTime,
4    Utc,
5};
6use pipedash_core::domain::pairing::{
7    display_pairing_code,
8    pairing_link,
9};
10use pipedash_core::domain::{
11    PairedDevice,
12    ServerUrl,
13};
14use qrcode::render::svg;
15use qrcode::QrCode;
16use serde::Serialize;
17
18use crate::error::{
19    ApiError,
20    ApiResult,
21    AppError,
22};
23use crate::state::AppState;
24
25#[derive(Debug, Serialize)]
26pub struct PairingResponse {
27    pub code: String,
28    pub link: String,
29    pub server_url: String,
30    pub expires_at: DateTime<Utc>,
31    pub loopback: bool,
32    pub qr_svg: String,
33}
34
35#[derive(Debug, Serialize)]
36pub struct DeviceResponse {
37    pub id: String,
38    pub name: String,
39    pub platform: String,
40    pub created_at: DateTime<Utc>,
41    pub last_seen_at: Option<DateTime<Utc>>,
42    pub current: bool,
43    pub removable: bool,
44    pub push: bool,
45}
46
47pub fn device_response(device: PairedDevice, current: Option<&str>, push: bool) -> DeviceResponse {
48    let current = current == Some(device.id.as_str());
49    DeviceResponse {
50        removable: !current && !crate::demo::enabled(),
51        current,
52        push,
53        id: device.id,
54        name: device.name,
55        platform: device.platform,
56        created_at: device.created_at,
57        last_seen_at: device.last_seen_at,
58    }
59}
60
61pub fn create_pairing(state: &AppState, server_url: &str) -> ApiResult<PairingResponse> {
62    let server = ServerUrl::parse(server_url).map_err(|reason| {
63        AppError::new(
64            StatusCode::UNPROCESSABLE_ENTITY,
65            ApiError::new(
66                "INVALID_SERVER_URL",
67                format!("Invalid server address: {reason}"),
68            ),
69        )
70    })?;
71    let (code, expires_at) = state.pairing_codes.issue()?;
72    let link = pairing_link(&server.url, &code);
73    let qr_svg = QrCode::new(link.as_bytes())
74        .map_err(|error| AppError::internal(error.to_string()))?
75        .render::<svg::Color>()
76        .min_dimensions(240, 240)
77        .dark_color(svg::Color("#000000"))
78        .light_color(svg::Color("#ffffff"))
79        .build();
80
81    Ok(PairingResponse {
82        code: display_pairing_code(&code),
83        loopback: server.is_loopback(),
84        server_url: server.url,
85        link,
86        expires_at,
87        qr_svg,
88    })
89}