1use axum::extract::{Path, State};
9use axum::routing::{get, post};
10use axum::{Json, Router};
11use serde::Deserialize;
12use serde_json::{json, Value};
13
14use crate::auth::{self, Principal};
15use crate::error::AppResult;
16use crate::models::{CameraOnvif, PtzPreset};
17use crate::routes::cameras::load_camera;
18use crate::services::onvif;
19use crate::state::AppState;
20
21pub fn router() -> Router<AppState> {
22 Router::new()
23 .route("/api/v1/onvif/discover", post(discover))
24 .route("/api/v1/cameras/{id}/onvif", get(get_onvif))
25 .route("/api/v1/cameras/{id}/onvif/probe", post(probe))
26 .route("/api/v1/cameras/{id}/ptz/presets", get(list_presets))
27 .route(
28 "/api/v1/cameras/{id}/ptz/presets/refresh",
29 post(refresh_presets),
30 )
31 .route("/api/v1/cameras/{id}/ptz/continuous", post(continuous_move))
32 .route("/api/v1/cameras/{id}/ptz/stop", post(ptz_stop))
33 .route("/api/v1/cameras/{id}/ptz/goto_preset", post(goto_preset))
34}
35
36async fn discover(State(st): State<AppState>, principal: Principal) -> AppResult<Json<Value>> {
39 principal.require(principal.can_manage_registry(), "run ONVIF discovery")?;
40 let devices = onvif::discover(&st.cfg).await?;
41 auth::audit(
42 &st.pool,
43 &principal,
44 "onvif_discover",
45 "onvif",
46 "discovery",
47 json!({ "found": devices.len() }),
48 )
49 .await;
50 Ok(Json(json!({
51 "found": devices.len(),
52 "devices": devices,
53 })))
54}
55
56async fn get_onvif(
59 State(st): State<AppState>,
60 Path(id): Path<String>,
61 principal: Principal,
62) -> AppResult<Json<CameraOnvif>> {
63 principal.require(principal.can_view(), "view ONVIF profile")?;
64 let _ = load_camera(&st.pool, &id).await?;
65 Ok(Json(onvif::load_onvif(&st.pool, &id).await?))
66}
67
68#[derive(Debug, Default, Deserialize)]
69struct ProbeRequest {
70 device_url: Option<String>,
73}
74
75async fn probe(
76 State(st): State<AppState>,
77 Path(id): Path<String>,
78 principal: Principal,
79 body: Option<Json<ProbeRequest>>,
80) -> AppResult<Json<CameraOnvif>> {
81 principal.require(principal.can_manage_registry(), "probe ONVIF devices")?;
82 let _ = load_camera(&st.pool, &id).await?;
83 let device_url = body.and_then(|Json(b)| b.device_url);
84 let onvif = onvif::probe(&st, &id, device_url).await?;
85 auth::audit(
86 &st.pool,
87 &principal,
88 "onvif_probe",
89 "camera",
90 &id,
91 json!({
92 "manufacturer": onvif.manufacturer,
93 "model": onvif.model,
94 "ptz_enabled": onvif.ptz_enabled,
95 }),
96 )
97 .await;
98 Ok(Json(onvif))
99}
100
101async fn list_presets(
104 State(st): State<AppState>,
105 Path(id): Path<String>,
106 principal: Principal,
107) -> AppResult<Json<Vec<PtzPreset>>> {
108 principal.require(principal.can_view(), "view PTZ presets")?;
109 let _ = load_camera(&st.pool, &id).await?;
110 let rows = sqlx::query_as::<_, PtzPreset>(
111 "SELECT * FROM camera_ptz_presets WHERE camera_id = ? ORDER BY token ASC",
112 )
113 .bind(&id)
114 .fetch_all(&st.pool)
115 .await?;
116 Ok(Json(rows))
117}
118
119async fn refresh_presets(
120 State(st): State<AppState>,
121 Path(id): Path<String>,
122 principal: Principal,
123) -> AppResult<Json<Vec<PtzPreset>>> {
124 principal.require(principal.can_manage_registry(), "refresh PTZ presets")?;
125 let _ = load_camera(&st.pool, &id).await?;
126 let presets = onvif::get_presets(&st, &id).await?;
127 auth::audit(
128 &st.pool,
129 &principal,
130 "ptz_refresh_presets",
131 "camera",
132 &id,
133 json!({ "count": presets.len() }),
134 )
135 .await;
136 Ok(Json(presets))
137}
138
139#[derive(Debug, Deserialize)]
142struct ContinuousMoveRequest {
143 #[serde(default)]
144 pan: f64,
145 #[serde(default)]
146 tilt: f64,
147 #[serde(default)]
148 zoom: f64,
149}
150
151async fn continuous_move(
152 State(st): State<AppState>,
153 Path(id): Path<String>,
154 principal: Principal,
155 Json(body): Json<ContinuousMoveRequest>,
156) -> AppResult<Json<Value>> {
157 principal.require(principal.can_manage_registry(), "control PTZ")?;
158 let _ = load_camera(&st.pool, &id).await?;
159 onvif::continuous_move(&st, &id, body.pan, body.tilt, body.zoom).await?;
160 auth::audit(
161 &st.pool,
162 &principal,
163 "ptz_continuous_move",
164 "camera",
165 &id,
166 json!({ "pan": body.pan, "tilt": body.tilt, "zoom": body.zoom }),
167 )
168 .await;
169 Ok(Json(json!({ "ok": true })))
170}
171
172async fn ptz_stop(
173 State(st): State<AppState>,
174 Path(id): Path<String>,
175 principal: Principal,
176) -> AppResult<Json<Value>> {
177 principal.require(principal.can_manage_registry(), "control PTZ")?;
178 let _ = load_camera(&st.pool, &id).await?;
179 onvif::stop(&st, &id).await?;
180 auth::audit(&st.pool, &principal, "ptz_stop", "camera", &id, json!({})).await;
181 Ok(Json(json!({ "ok": true })))
182}
183
184#[derive(Debug, Deserialize)]
185struct GotoPresetRequest {
186 token: String,
187}
188
189async fn goto_preset(
190 State(st): State<AppState>,
191 Path(id): Path<String>,
192 principal: Principal,
193 Json(body): Json<GotoPresetRequest>,
194) -> AppResult<Json<Value>> {
195 principal.require(principal.can_manage_registry(), "control PTZ")?;
196 let _ = load_camera(&st.pool, &id).await?;
197 onvif::goto_preset(&st, &id, &body.token).await?;
198 auth::audit(
199 &st.pool,
200 &principal,
201 "ptz_goto_preset",
202 "camera",
203 &id,
204 json!({ "token": body.token }),
205 )
206 .await;
207 Ok(Json(json!({ "ok": true })))
208}