1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use sqlx::types::Json;
5use sqlx::FromRow;
6
7use crate::camera_url;
8
9#[derive(Debug, Clone, FromRow)]
11pub struct Camera {
12 pub id: String,
13 pub site_id: Option<String>,
14 pub name: String,
15 pub vendor: String,
16 pub model: Option<String>,
17 pub address: Option<String>,
18 pub rtsp_port: i64,
19 pub username: Option<String>,
20 pub password: Option<String>,
21 pub main_stream_url: Option<String>,
22 pub sub_stream_url: Option<String>,
23 pub record_stream: String,
24 pub codec: Option<String>,
25 pub resolution_main: Option<String>,
26 pub resolution_sub: Option<String>,
27 pub fps_main: Option<i64>,
28 pub fps_sub: Option<i64>,
29 pub capabilities: Json<Value>,
30 pub record_enabled: bool,
31 pub segment_seconds: i64,
32 pub retention_hours: i64,
33 pub storage_quota_bytes: Option<i64>,
35 pub record_audio: bool,
37 pub record_mode: String,
39 pub pre_roll_seconds: i64,
41 pub post_roll_seconds: i64,
43 pub mirror_enabled: bool,
46 pub anr_enabled: bool,
48 pub anr_replay_url_template: Option<String>,
51 pub enabled: bool,
52 pub created_at: DateTime<Utc>,
53 pub updated_at: DateTime<Utc>,
54}
55
56impl Camera {
57 pub fn should_record(&self) -> bool {
59 self.enabled && self.record_enabled
60 }
61}
62
63#[derive(Debug, Clone, Serialize)]
65pub struct CameraView {
66 pub id: String,
67 pub site_id: Option<String>,
68 pub name: String,
69 pub vendor: String,
70 pub model: Option<String>,
71 pub address: Option<String>,
72 pub rtsp_port: i64,
73 pub username: Option<String>,
74 pub has_password: bool,
75 pub record_stream: String,
76 pub record_url_masked: Option<String>,
78 pub codec: Option<String>,
79 pub resolution_main: Option<String>,
80 pub resolution_sub: Option<String>,
81 pub fps_main: Option<i64>,
82 pub fps_sub: Option<i64>,
83 pub capabilities: Value,
84 pub record_enabled: bool,
85 pub segment_seconds: i64,
86 pub retention_hours: i64,
87 pub storage_quota_bytes: Option<i64>,
88 pub record_audio: bool,
89 pub record_mode: String,
90 pub pre_roll_seconds: i64,
91 pub post_roll_seconds: i64,
92 pub mirror_enabled: bool,
93 pub anr_enabled: bool,
94 pub anr_replay_url_template: Option<String>,
95 pub enabled: bool,
96 pub created_at: DateTime<Utc>,
97 pub updated_at: DateTime<Utc>,
98}
99
100impl From<Camera> for CameraView {
101 fn from(c: Camera) -> Self {
102 let record_url_masked = camera_url::record_url(&c).map(|u| camera_url::mask_url(&u));
103 CameraView {
104 id: c.id,
105 site_id: c.site_id,
106 name: c.name,
107 vendor: c.vendor,
108 model: c.model,
109 address: c.address,
110 rtsp_port: c.rtsp_port,
111 username: c.username,
112 has_password: c
113 .password
114 .as_deref()
115 .map(|p| !p.is_empty())
116 .unwrap_or(false),
117 record_stream: c.record_stream,
118 record_url_masked,
119 codec: c.codec,
120 resolution_main: c.resolution_main,
121 resolution_sub: c.resolution_sub,
122 fps_main: c.fps_main,
123 fps_sub: c.fps_sub,
124 capabilities: c.capabilities.0,
125 record_enabled: c.record_enabled,
126 segment_seconds: c.segment_seconds,
127 retention_hours: c.retention_hours,
128 storage_quota_bytes: c.storage_quota_bytes,
129 record_audio: c.record_audio,
130 record_mode: c.record_mode,
131 pre_roll_seconds: c.pre_roll_seconds,
132 post_roll_seconds: c.post_roll_seconds,
133 mirror_enabled: c.mirror_enabled,
134 anr_enabled: c.anr_enabled,
135 anr_replay_url_template: c.anr_replay_url_template,
136 enabled: c.enabled,
137 created_at: c.created_at,
138 updated_at: c.updated_at,
139 }
140 }
141}
142
143#[derive(Debug, Deserialize)]
145pub struct CameraCreate {
146 pub id: Option<String>,
147 pub name: String,
148 pub site_id: Option<String>,
149 #[serde(default = "default_vendor")]
150 pub vendor: String,
151 pub model: Option<String>,
152 pub address: Option<String>,
153 pub rtsp_port: Option<i64>,
154 pub username: Option<String>,
155 pub password: Option<String>,
156 pub main_stream_url: Option<String>,
157 pub sub_stream_url: Option<String>,
158 pub record_stream: Option<String>,
159 pub capabilities: Option<Value>,
160 pub record_enabled: Option<bool>,
161 pub segment_seconds: Option<i64>,
162 pub retention_hours: Option<i64>,
163 pub storage_quota_bytes: Option<i64>,
164 pub record_audio: Option<bool>,
165 pub record_mode: Option<String>,
166 pub pre_roll_seconds: Option<i64>,
167 pub post_roll_seconds: Option<i64>,
168 pub mirror_enabled: Option<bool>,
169 pub anr_enabled: Option<bool>,
170 pub anr_replay_url_template: Option<String>,
171 pub enabled: Option<bool>,
172}
173
174fn default_vendor() -> String {
175 "generic".to_string()
176}
177
178#[derive(Debug, Deserialize, Default)]
180pub struct CameraUpdate {
181 pub name: Option<String>,
182 pub site_id: Option<String>,
183 pub vendor: Option<String>,
184 pub model: Option<String>,
185 pub address: Option<String>,
186 pub rtsp_port: Option<i64>,
187 pub username: Option<String>,
188 pub password: Option<String>,
189 pub main_stream_url: Option<String>,
190 pub sub_stream_url: Option<String>,
191 pub record_stream: Option<String>,
192 pub capabilities: Option<Value>,
193 pub record_enabled: Option<bool>,
194 pub segment_seconds: Option<i64>,
195 pub retention_hours: Option<i64>,
196 pub storage_quota_bytes: Option<i64>,
197 pub record_audio: Option<bool>,
198 pub record_mode: Option<String>,
199 pub pre_roll_seconds: Option<i64>,
200 pub post_roll_seconds: Option<i64>,
201 pub mirror_enabled: Option<bool>,
202 pub anr_enabled: Option<bool>,
203 pub anr_replay_url_template: Option<String>,
204 pub enabled: Option<bool>,
205}
206
207#[derive(Debug, Clone, Serialize, FromRow)]
208pub struct Segment {
209 pub id: String,
210 pub camera_id: String,
211 pub path: String,
212 pub start_time: DateTime<Utc>,
213 pub end_time: DateTime<Utc>,
214 pub duration_s: f64,
215 pub codec: Option<String>,
216 pub width: Option<i64>,
217 pub height: Option<i64>,
218 pub size_bytes: i64,
219 pub container: String,
220 pub locked: bool,
222 pub evidence_locked: bool,
225 pub incident_id: Option<String>,
226 pub created_at: DateTime<Utc>,
227}
228
229#[derive(Debug, Clone, Serialize, FromRow)]
233pub struct RecordingGap {
234 pub id: String,
235 pub camera_id: String,
236 pub gap_start: DateTime<Utc>,
237 pub gap_end: DateTime<Utc>,
238 pub gap_seconds: i64,
239 pub fill_state: String,
240 pub fill_attempts: i64,
241 pub last_attempt_at: Option<DateTime<Utc>>,
242 pub filled_at: Option<DateTime<Utc>>,
243 pub created_at: DateTime<Utc>,
244}
245
246#[derive(Debug, Clone, Serialize, FromRow)]
247pub struct CameraStatus {
248 pub camera_id: String,
249 pub state: String,
250 pub last_segment_at: Option<DateTime<Utc>>,
251 pub last_started_at: Option<DateTime<Utc>>,
252 pub reconnect_count: i64,
253 pub segments_written: i64,
254 pub fps_observed: Option<f64>,
255 pub bitrate_kbps: Option<f64>,
256 pub last_error: Option<String>,
257 pub recorder_pid: Option<i64>,
258 pub updated_at: DateTime<Utc>,
259}
260
261#[derive(Debug, Clone, Serialize, FromRow)]
262pub struct Event {
263 pub id: String,
264 pub camera_id: Option<String>,
265 pub site_id: Option<String>,
266 pub event_type: String,
267 pub severity: String,
268 pub timestamp: DateTime<Utc>,
269 pub payload: Json<Value>,
270 pub created_at: DateTime<Utc>,
271}
272
273#[derive(Debug, Clone, Serialize, FromRow)]
277pub struct AiTask {
278 pub id: String,
279 pub camera_id: String,
280 pub task_type: String,
281 pub enabled: bool,
282 pub stream_profile: String,
283 pub fps: f64,
284 pub width: i64,
285 pub config: Json<Value>,
286 pub created_at: DateTime<Utc>,
287 pub updated_at: DateTime<Utc>,
288}
289
290#[derive(Debug, Deserialize)]
291pub struct AiTaskCreate {
292 pub task_type: String,
293 pub stream_profile: Option<String>,
294 pub fps: Option<f64>,
295 pub width: Option<i64>,
296 pub config: Option<Value>,
297 pub enabled: Option<bool>,
298}
299
300#[derive(Debug, Deserialize, Default)]
301pub struct AiTaskUpdate {
302 pub task_type: Option<String>,
303 pub stream_profile: Option<String>,
304 pub fps: Option<f64>,
305 pub width: Option<i64>,
306 pub config: Option<Value>,
307 pub enabled: Option<bool>,
308}
309
310#[derive(Debug, Clone, Serialize, FromRow)]
312pub struct Detection {
313 pub id: String,
314 pub camera_id: String,
315 pub task_type: String,
316 pub timestamp: DateTime<Utc>,
317 pub label: Option<String>,
318 pub confidence: Option<f64>,
319 pub bbox: Option<Json<Value>>,
320 pub track_id: Option<String>,
321 pub attributes: Json<Value>,
322 pub frame_id: Option<String>,
324 pub created_at: DateTime<Utc>,
325}
326
327#[derive(Debug, Deserialize)]
329pub struct DetectionIngest {
330 pub label: Option<String>,
331 pub confidence: Option<f64>,
332 pub bbox: Option<Value>,
333 pub track_id: Option<String>,
334 pub attributes: Option<Value>,
335}
336
337#[derive(Debug, Deserialize)]
339pub struct IngestEvent {
340 pub event_type: String,
341 pub severity: Option<String>,
342 pub payload: Option<Value>,
343}
344
345#[derive(Debug, Deserialize)]
347pub struct AiIngest {
348 pub camera_id: String,
349 pub task_type: String,
350 pub timestamp: Option<String>,
351 pub frame_id: Option<String>,
355 #[serde(default)]
356 pub detections: Vec<DetectionIngest>,
357 pub event: Option<IngestEvent>,
358}
359
360#[derive(Debug, Clone, Serialize, FromRow)]
364pub struct Zone {
365 pub id: String,
366 pub camera_id: String,
367 pub name: String,
368 pub kind: String,
369 pub polygon: Json<Value>,
371 pub dwell_seconds: f64,
372 pub labels: Json<Value>,
374 pub severity: String,
375 pub config: Json<Value>,
376 pub enabled: bool,
377 pub created_at: DateTime<Utc>,
378 pub updated_at: DateTime<Utc>,
379}
380
381#[derive(Debug, Deserialize)]
382pub struct ZoneCreate {
383 pub name: String,
384 pub kind: Option<String>,
385 pub polygon: Value,
386 pub dwell_seconds: Option<f64>,
387 pub labels: Option<Value>,
388 pub severity: Option<String>,
389 pub config: Option<Value>,
390 pub enabled: Option<bool>,
391}
392
393#[derive(Debug, Deserialize, Default)]
394pub struct ZoneUpdate {
395 pub name: Option<String>,
396 pub kind: Option<String>,
397 pub polygon: Option<Value>,
398 pub dwell_seconds: Option<f64>,
399 pub labels: Option<Value>,
400 pub severity: Option<String>,
401 pub config: Option<Value>,
402 pub enabled: Option<bool>,
403}
404
405#[derive(Debug, Clone, Serialize, FromRow)]
406pub struct ZoneEvent {
407 pub id: String,
408 pub camera_id: String,
409 pub zone_id: String,
410 pub zone_name: String,
411 pub track_id: Option<String>,
412 pub event_type: String,
413 pub label: Option<String>,
414 pub timestamp: DateTime<Utc>,
415 pub dwell_seconds: Option<f64>,
416 pub evidence_path: Option<String>,
417 pub created_at: DateTime<Utc>,
418}
419
420#[derive(Debug, Clone, FromRow)]
424pub struct User {
425 pub id: String,
426 pub username: String,
427 pub password_hash: String,
428 pub role: String,
429 pub display_name: Option<String>,
430 pub active: bool,
431 pub created_at: DateTime<Utc>,
432 pub updated_at: DateTime<Utc>,
433}
434
435#[derive(Debug, Clone, Serialize)]
436pub struct UserView {
437 pub id: String,
438 pub username: String,
439 pub role: String,
440 pub display_name: Option<String>,
441 pub active: bool,
442 pub created_at: DateTime<Utc>,
443 pub updated_at: DateTime<Utc>,
444}
445
446impl From<User> for UserView {
447 fn from(u: User) -> Self {
448 UserView {
449 id: u.id,
450 username: u.username,
451 role: u.role,
452 display_name: u.display_name,
453 active: u.active,
454 created_at: u.created_at,
455 updated_at: u.updated_at,
456 }
457 }
458}
459
460#[derive(Debug, Deserialize)]
461pub struct UserCreate {
462 pub username: String,
463 pub password: String,
464 pub role: Option<String>,
465 pub display_name: Option<String>,
466 pub active: Option<bool>,
467}
468
469#[derive(Debug, Deserialize, Default)]
470pub struct UserUpdate {
471 pub password: Option<String>,
472 pub role: Option<String>,
473 pub display_name: Option<String>,
474 pub active: Option<bool>,
475}
476
477#[derive(Debug, Deserialize)]
478pub struct LoginRequest {
479 pub username: String,
480 pub password: String,
481}
482
483#[derive(Debug, Clone, FromRow)]
484pub struct ApiKey {
485 pub id: String,
486 pub name: String,
487 pub key_hash: String,
489 pub key_prefix: String,
490 pub role: String,
491 pub active: bool,
492 pub last_used_at: Option<DateTime<Utc>>,
493 pub created_at: DateTime<Utc>,
494}
495
496#[derive(Debug, Clone, Serialize)]
497pub struct ApiKeyView {
498 pub id: String,
499 pub name: String,
500 pub key_prefix: String,
501 pub role: String,
502 pub active: bool,
503 pub last_used_at: Option<DateTime<Utc>>,
504 pub created_at: DateTime<Utc>,
505}
506
507impl From<ApiKey> for ApiKeyView {
508 fn from(k: ApiKey) -> Self {
509 ApiKeyView {
510 id: k.id,
511 name: k.name,
512 key_prefix: k.key_prefix,
513 role: k.role,
514 active: k.active,
515 last_used_at: k.last_used_at,
516 created_at: k.created_at,
517 }
518 }
519}
520
521#[derive(Debug, Deserialize)]
522pub struct ApiKeyCreate {
523 pub name: String,
524 pub role: Option<String>,
525}
526
527#[derive(Debug, Clone, Serialize, FromRow)]
531pub struct SnapshotSchedule {
532 pub id: String,
533 pub camera_id: String,
534 pub interval_seconds: i64,
535 pub enabled: bool,
536 pub last_fired_at: Option<DateTime<Utc>>,
537 pub created_at: DateTime<Utc>,
538 pub updated_at: DateTime<Utc>,
539}
540
541#[derive(Debug, Deserialize)]
542pub struct SnapshotScheduleCreate {
543 pub interval_seconds: Option<i64>,
544 pub enabled: Option<bool>,
545}
546
547#[derive(Debug, Deserialize, Default)]
548pub struct SnapshotScheduleUpdate {
549 pub interval_seconds: Option<i64>,
550 pub enabled: Option<bool>,
551}
552
553#[derive(Debug, Clone, Serialize, FromRow)]
555pub struct PersistedSnapshot {
556 pub id: String,
557 pub camera_id: String,
558 pub schedule_id: Option<String>,
559 pub path: String,
560 pub taken_at: DateTime<Utc>,
561 pub size_bytes: i64,
562 pub created_at: DateTime<Utc>,
563}
564
565#[derive(Debug, Clone, Serialize, FromRow)]
573pub struct RecordSchedule {
574 pub id: String,
575 pub camera_id: String,
576 pub days: Json<Value>,
577 pub time_start: String,
578 pub time_end: String,
579 pub enabled: bool,
580 pub created_at: DateTime<Utc>,
581 pub updated_at: DateTime<Utc>,
582}
583
584#[derive(Debug, Deserialize)]
585pub struct RecordScheduleCreate {
586 pub days: Value,
588 pub time_start: String,
590 pub time_end: String,
592 pub enabled: Option<bool>,
593}
594
595#[derive(Debug, Deserialize, Default)]
596pub struct RecordScheduleUpdate {
597 pub days: Option<Value>,
598 pub time_start: Option<String>,
599 pub time_end: Option<String>,
600 pub enabled: Option<bool>,
601}
602
603pub const BACKUP_SECRET_KEYS: &[&str] = &["pass", "password", "secret_key", "secret"];
608
609#[derive(Debug, Clone, FromRow)]
612pub struct BackupDestination {
613 pub id: String,
614 pub name: String,
615 pub kind: String,
617 pub config: Json<Value>,
618 pub enabled: bool,
619 pub created_at: DateTime<Utc>,
620 pub updated_at: DateTime<Utc>,
621}
622
623#[derive(Debug, Clone, Serialize)]
625pub struct BackupDestinationView {
626 pub id: String,
627 pub name: String,
628 pub kind: String,
629 pub config: Value,
631 pub has_credentials: bool,
633 pub enabled: bool,
634 pub created_at: DateTime<Utc>,
635 pub updated_at: DateTime<Utc>,
636}
637
638pub fn mask_backup_config(mut config: Value) -> (Value, bool) {
640 let mut has_credentials = false;
641 if let Some(obj) = config.as_object_mut() {
642 for key in BACKUP_SECRET_KEYS {
643 if let Some(v) = obj.get_mut(*key) {
644 if v.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
645 has_credentials = true;
646 *v = Value::String("***".to_string());
647 }
648 }
649 }
650 }
651 (config, has_credentials)
652}
653
654impl From<BackupDestination> for BackupDestinationView {
655 fn from(d: BackupDestination) -> Self {
656 let (config, has_credentials) = mask_backup_config(d.config.0);
657 BackupDestinationView {
658 id: d.id,
659 name: d.name,
660 kind: d.kind,
661 config,
662 has_credentials,
663 enabled: d.enabled,
664 created_at: d.created_at,
665 updated_at: d.updated_at,
666 }
667 }
668}
669
670#[derive(Debug, Deserialize)]
671pub struct BackupDestinationCreate {
672 pub name: String,
673 pub kind: String,
675 pub config: Option<Value>,
676 pub enabled: Option<bool>,
677}
678
679#[derive(Debug, Deserialize, Default)]
680pub struct BackupDestinationUpdate {
681 pub name: Option<String>,
682 pub kind: Option<String>,
683 pub config: Option<Value>,
684 pub enabled: Option<bool>,
685}
686
687#[derive(Debug, Clone, Serialize)]
689pub struct BackupTestResult {
690 pub ok: bool,
691 pub error: Option<String>,
692 pub latency_ms: i64,
693}
694
695#[derive(Debug, Clone, Serialize, FromRow)]
697pub struct BackupPolicy {
698 pub id: String,
699 pub name: String,
700 pub destination_id: String,
701 pub camera_ids: Json<Value>,
703 pub incident_lock_only: bool,
704 pub schedule_interval_s: i64,
705 pub lookback_hours: i64,
706 pub last_run_at: Option<DateTime<Utc>>,
707 pub last_job_id: Option<String>,
708 pub enabled: bool,
709 pub created_at: DateTime<Utc>,
710 pub updated_at: DateTime<Utc>,
711}
712
713#[derive(Debug, Deserialize)]
714pub struct BackupPolicyCreate {
715 pub name: String,
716 pub destination_id: String,
717 pub camera_ids: Option<Value>,
718 pub incident_lock_only: Option<bool>,
719 pub schedule_interval_s: Option<i64>,
720 pub lookback_hours: Option<i64>,
721 pub enabled: Option<bool>,
722}
723
724#[derive(Debug, Deserialize, Default)]
725pub struct BackupPolicyUpdate {
726 pub name: Option<String>,
727 pub destination_id: Option<String>,
728 pub camera_ids: Option<Value>,
729 pub incident_lock_only: Option<bool>,
730 pub schedule_interval_s: Option<i64>,
731 pub lookback_hours: Option<i64>,
732 pub enabled: Option<bool>,
733}
734
735#[derive(Debug, Clone, Serialize, FromRow)]
737pub struct BackupJob {
738 pub id: String,
739 pub policy_id: Option<String>,
740 pub destination_id: Option<String>,
741 pub kind: String,
743 pub camera_ids: Json<Value>,
744 pub from_time: Option<DateTime<Utc>>,
745 pub to_time: Option<DateTime<Utc>>,
746 pub incident_lock_only: bool,
747 pub status: String,
749 pub files_total: i64,
750 pub files_copied: i64,
751 pub bytes_copied: i64,
752 pub error: Option<String>,
753 pub output_path: Option<String>,
755 pub output_url: Option<String>,
757 pub started_at: Option<DateTime<Utc>>,
758 pub finished_at: Option<DateTime<Utc>>,
759 pub created_at: DateTime<Utc>,
760}
761
762#[derive(Debug, Clone, Serialize, FromRow)]
768pub struct CameraOnvif {
769 pub camera_id: String,
770 pub device_url: String,
771 pub manufacturer: Option<String>,
772 pub model: Option<String>,
773 pub firmware_version: Option<String>,
774 pub serial_number: Option<String>,
775 pub hardware_id: Option<String>,
776 pub scopes: Json<Value>,
777 pub media_url: Option<String>,
778 pub ptz_url: Option<String>,
779 pub profile_token: Option<String>,
780 pub ptz_node_token: Option<String>,
781 pub ptz_enabled: bool,
782 pub probed_at: DateTime<Utc>,
783}
784
785#[derive(Debug, Clone, Serialize, FromRow)]
787pub struct PtzPreset {
788 pub id: String,
789 pub camera_id: String,
790 pub token: String,
791 pub name: Option<String>,
792 pub fetched_at: DateTime<Utc>,
793}
794
795#[derive(Debug, Clone, FromRow, Serialize)]
801pub struct CameraIsapi {
802 pub camera_id: String,
803 pub device_name: Option<String>,
804 pub model: Option<String>,
805 pub firmware_version: Option<String>,
806 pub serial_number: Option<String>,
807 pub onvif_enabled: bool,
808 pub onvif_user_created: bool,
809 pub time_mode: Option<String>,
810 pub ntp_server: Option<String>,
811 pub fetched_at: DateTime<Utc>,
812}
813
814fn de_field_present<'de, D>(deserializer: D) -> Result<Option<Option<String>>, D::Error>
820where
821 D: serde::Deserializer<'de>,
822{
823 Ok(Some(Option::<String>::deserialize(deserializer)?))
824}
825
826pub fn mask_webhook_url(url: &str) -> Option<String> {
830 let url = url.trim();
831 if url.is_empty() {
832 return None;
833 }
834 match url.split_once("://") {
835 Some((scheme, rest)) => {
836 let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
837 let authority = &rest[..authority_end];
838 if authority_end < rest.len() {
839 Some(format!("{scheme}://{authority}/…"))
840 } else {
841 Some(format!("{scheme}://{authority}"))
842 }
843 }
844 None => Some("…".to_string()),
845 }
846}
847
848#[derive(Debug, Clone, FromRow)]
855pub struct WebhookSubscription {
856 pub id: String,
857 pub name: String,
858 pub url: String,
859 pub event_types: Json<Vec<String>>,
860 pub min_severity: String,
861 pub secret: Option<String>,
862 pub enabled: bool,
863 pub cursor_at: Option<DateTime<Utc>>,
864 pub created_at: DateTime<Utc>,
865 pub updated_at: DateTime<Utc>,
866}
867
868#[derive(Debug, Clone, Serialize)]
870pub struct WebhookSubscriptionView {
871 pub id: String,
872 pub name: String,
873 pub url: String,
874 pub event_types: Vec<String>,
875 pub min_severity: String,
876 pub has_secret: bool,
878 pub enabled: bool,
879 pub cursor_at: Option<DateTime<Utc>>,
880 pub created_at: DateTime<Utc>,
881 pub updated_at: DateTime<Utc>,
882}
883
884impl From<WebhookSubscription> for WebhookSubscriptionView {
885 fn from(s: WebhookSubscription) -> Self {
886 WebhookSubscriptionView {
887 id: s.id,
888 name: s.name,
889 url: s.url,
890 event_types: s.event_types.0,
891 min_severity: s.min_severity,
892 has_secret: s.secret.as_deref().map(|v| !v.is_empty()).unwrap_or(false),
893 enabled: s.enabled,
894 cursor_at: s.cursor_at,
895 created_at: s.created_at,
896 updated_at: s.updated_at,
897 }
898 }
899}
900
901#[derive(Debug, Deserialize)]
902pub struct WebhookSubscriptionCreate {
903 pub name: String,
904 pub url: String,
905 pub event_types: Option<Vec<String>>,
907 pub min_severity: Option<String>,
909 pub secret: Option<String>,
911 pub enabled: Option<bool>,
912}
913
914#[derive(Debug, Deserialize, Default)]
918pub struct WebhookSubscriptionUpdate {
919 pub name: Option<String>,
920 pub url: Option<String>,
921 pub event_types: Option<Vec<String>>,
922 pub min_severity: Option<String>,
923 #[serde(default, deserialize_with = "de_field_present")]
924 pub secret: Option<Option<String>>,
925 pub enabled: Option<bool>,
926}
927
928#[derive(Debug, Clone, Serialize, FromRow)]
930pub struct WebhookDelivery {
931 pub id: String,
932 pub subscription_id: String,
933 pub event_id: Option<String>,
934 pub event_type: Option<String>,
935 pub status: String,
936 pub attempts: i64,
937 pub response_code: Option<i64>,
938 pub error: Option<String>,
939 pub created_at: DateTime<Utc>,
940 pub delivered_at: Option<DateTime<Utc>>,
941}
942
943#[derive(Debug, Deserialize)]
945pub struct ArchiveExportRequest {
946 #[serde(default)]
948 pub camera_ids: Vec<String>,
949 pub from: Option<String>,
950 pub to: Option<String>,
951 pub incident_lock_only: Option<bool>,
952 pub trim: Option<bool>,
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959
960 #[test]
961 fn mask_webhook_url_hides_path_and_token() {
962 assert_eq!(
964 mask_webhook_url("https://hooks.slack.com/services/T000/B000/XXXXSECRET"),
965 Some("https://hooks.slack.com/…".to_string())
966 );
967 assert_eq!(
968 mask_webhook_url("https://example.com:8443/alert?token=abc"),
969 Some("https://example.com:8443/…".to_string())
970 );
971 assert_eq!(
973 mask_webhook_url("https://example.com"),
974 Some("https://example.com".to_string())
975 );
976 assert_eq!(mask_webhook_url(" "), None);
978 assert_eq!(mask_webhook_url("not-a-url"), Some("…".to_string()));
979 }
980
981 #[test]
982 fn webhook_update_secret_is_three_state() {
983 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"enabled": true}"#).unwrap();
985 assert!(u.secret.is_none());
986 assert_eq!(u.enabled, Some(true));
987 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"secret": null}"#).unwrap();
989 assert_eq!(u.secret, Some(None));
990 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"secret": "s3cr3t"}"#).unwrap();
992 assert_eq!(u.secret, Some(Some("s3cr3t".to_string())));
993 }
994}