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, Clone, Deserialize, Serialize)]
330pub struct DetectionIngest {
331 pub label: Option<String>,
332 pub confidence: Option<f64>,
333 pub bbox: Option<Value>,
334 pub track_id: Option<String>,
335 pub attributes: Option<Value>,
336}
337
338#[derive(Debug, Deserialize)]
340pub struct IngestEvent {
341 pub event_type: String,
342 pub severity: Option<String>,
343 pub payload: Option<Value>,
344}
345
346#[derive(Debug, Deserialize)]
348pub struct AiIngest {
349 pub camera_id: String,
350 pub task_type: String,
351 pub timestamp: Option<String>,
352 pub frame_id: Option<String>,
356 #[serde(default)]
357 pub detections: Vec<DetectionIngest>,
358 pub event: Option<IngestEvent>,
359}
360
361#[derive(Debug, Clone, Serialize, FromRow)]
365pub struct Zone {
366 pub id: String,
367 pub camera_id: String,
368 pub name: String,
369 pub kind: String,
370 pub polygon: Json<Value>,
372 pub dwell_seconds: f64,
373 pub labels: Json<Value>,
375 pub severity: String,
376 pub config: Json<Value>,
377 pub enabled: bool,
378 pub created_at: DateTime<Utc>,
379 pub updated_at: DateTime<Utc>,
380}
381
382#[derive(Debug, Deserialize)]
383pub struct ZoneCreate {
384 pub name: String,
385 pub kind: Option<String>,
386 pub polygon: Value,
387 pub dwell_seconds: Option<f64>,
388 pub labels: Option<Value>,
389 pub severity: Option<String>,
390 pub config: Option<Value>,
391 pub enabled: Option<bool>,
392}
393
394#[derive(Debug, Deserialize, Default)]
395pub struct ZoneUpdate {
396 pub name: Option<String>,
397 pub kind: Option<String>,
398 pub polygon: Option<Value>,
399 pub dwell_seconds: Option<f64>,
400 pub labels: Option<Value>,
401 pub severity: Option<String>,
402 pub config: Option<Value>,
403 pub enabled: Option<bool>,
404}
405
406#[derive(Debug, Clone, Serialize, FromRow)]
407pub struct ZoneEvent {
408 pub id: String,
409 pub camera_id: String,
410 pub zone_id: String,
411 pub zone_name: String,
412 pub track_id: Option<String>,
413 pub event_type: String,
414 pub label: Option<String>,
415 pub timestamp: DateTime<Utc>,
416 pub dwell_seconds: Option<f64>,
417 pub evidence_path: Option<String>,
418 pub created_at: DateTime<Utc>,
419}
420
421#[derive(Debug, Clone, FromRow)]
425pub struct User {
426 pub id: String,
427 pub username: String,
428 pub password_hash: String,
429 pub role: String,
430 pub display_name: Option<String>,
431 pub active: bool,
432 pub created_at: DateTime<Utc>,
433 pub updated_at: DateTime<Utc>,
434}
435
436#[derive(Debug, Clone, Serialize)]
437pub struct UserView {
438 pub id: String,
439 pub username: String,
440 pub role: String,
441 pub display_name: Option<String>,
442 pub active: bool,
443 pub created_at: DateTime<Utc>,
444 pub updated_at: DateTime<Utc>,
445}
446
447impl From<User> for UserView {
448 fn from(u: User) -> Self {
449 UserView {
450 id: u.id,
451 username: u.username,
452 role: u.role,
453 display_name: u.display_name,
454 active: u.active,
455 created_at: u.created_at,
456 updated_at: u.updated_at,
457 }
458 }
459}
460
461#[derive(Debug, Deserialize)]
462pub struct UserCreate {
463 pub username: String,
464 pub password: String,
465 pub role: Option<String>,
466 pub display_name: Option<String>,
467 pub active: Option<bool>,
468}
469
470#[derive(Debug, Deserialize, Default)]
471pub struct UserUpdate {
472 pub password: Option<String>,
473 pub role: Option<String>,
474 pub display_name: Option<String>,
475 pub active: Option<bool>,
476}
477
478#[derive(Debug, Deserialize)]
479pub struct LoginRequest {
480 pub username: String,
481 pub password: String,
482}
483
484#[derive(Debug, Clone, FromRow)]
485pub struct ApiKey {
486 pub id: String,
487 pub name: String,
488 pub key_hash: String,
490 pub key_prefix: String,
491 pub role: String,
492 pub active: bool,
493 pub last_used_at: Option<DateTime<Utc>>,
494 pub created_at: DateTime<Utc>,
495}
496
497#[derive(Debug, Clone, Serialize)]
498pub struct ApiKeyView {
499 pub id: String,
500 pub name: String,
501 pub key_prefix: String,
502 pub role: String,
503 pub active: bool,
504 pub last_used_at: Option<DateTime<Utc>>,
505 pub created_at: DateTime<Utc>,
506}
507
508impl From<ApiKey> for ApiKeyView {
509 fn from(k: ApiKey) -> Self {
510 ApiKeyView {
511 id: k.id,
512 name: k.name,
513 key_prefix: k.key_prefix,
514 role: k.role,
515 active: k.active,
516 last_used_at: k.last_used_at,
517 created_at: k.created_at,
518 }
519 }
520}
521
522#[derive(Debug, Deserialize)]
523pub struct ApiKeyCreate {
524 pub name: String,
525 pub role: Option<String>,
526}
527
528#[derive(Debug, Clone, Serialize, FromRow)]
532pub struct SnapshotSchedule {
533 pub id: String,
534 pub camera_id: String,
535 pub interval_seconds: i64,
536 pub enabled: bool,
537 pub last_fired_at: Option<DateTime<Utc>>,
538 pub created_at: DateTime<Utc>,
539 pub updated_at: DateTime<Utc>,
540}
541
542#[derive(Debug, Deserialize)]
543pub struct SnapshotScheduleCreate {
544 pub interval_seconds: Option<i64>,
545 pub enabled: Option<bool>,
546}
547
548#[derive(Debug, Deserialize, Default)]
549pub struct SnapshotScheduleUpdate {
550 pub interval_seconds: Option<i64>,
551 pub enabled: Option<bool>,
552}
553
554#[derive(Debug, Clone, Serialize, FromRow)]
556pub struct PersistedSnapshot {
557 pub id: String,
558 pub camera_id: String,
559 pub schedule_id: Option<String>,
560 pub path: String,
561 pub taken_at: DateTime<Utc>,
562 pub size_bytes: i64,
563 pub created_at: DateTime<Utc>,
564}
565
566#[derive(Debug, Clone, Serialize, FromRow)]
574pub struct RecordSchedule {
575 pub id: String,
576 pub camera_id: String,
577 pub days: Json<Value>,
578 pub time_start: String,
579 pub time_end: String,
580 pub enabled: bool,
581 pub created_at: DateTime<Utc>,
582 pub updated_at: DateTime<Utc>,
583}
584
585#[derive(Debug, Deserialize)]
586pub struct RecordScheduleCreate {
587 pub days: Value,
589 pub time_start: String,
591 pub time_end: String,
593 pub enabled: Option<bool>,
594}
595
596#[derive(Debug, Deserialize, Default)]
597pub struct RecordScheduleUpdate {
598 pub days: Option<Value>,
599 pub time_start: Option<String>,
600 pub time_end: Option<String>,
601 pub enabled: Option<bool>,
602}
603
604pub const BACKUP_SECRET_KEYS: &[&str] = &["pass", "password", "secret_key", "secret"];
609
610#[derive(Debug, Clone, FromRow)]
613pub struct BackupDestination {
614 pub id: String,
615 pub name: String,
616 pub kind: String,
618 pub config: Json<Value>,
619 pub enabled: bool,
620 pub created_at: DateTime<Utc>,
621 pub updated_at: DateTime<Utc>,
622}
623
624#[derive(Debug, Clone, Serialize)]
626pub struct BackupDestinationView {
627 pub id: String,
628 pub name: String,
629 pub kind: String,
630 pub config: Value,
632 pub has_credentials: bool,
634 pub enabled: bool,
635 pub created_at: DateTime<Utc>,
636 pub updated_at: DateTime<Utc>,
637}
638
639pub fn mask_backup_config(mut config: Value) -> (Value, bool) {
641 let mut has_credentials = false;
642 if let Some(obj) = config.as_object_mut() {
643 for key in BACKUP_SECRET_KEYS {
644 if let Some(v) = obj.get_mut(*key) {
645 if v.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
646 has_credentials = true;
647 *v = Value::String("***".to_string());
648 }
649 }
650 }
651 }
652 (config, has_credentials)
653}
654
655impl From<BackupDestination> for BackupDestinationView {
656 fn from(d: BackupDestination) -> Self {
657 let (config, has_credentials) = mask_backup_config(d.config.0);
658 BackupDestinationView {
659 id: d.id,
660 name: d.name,
661 kind: d.kind,
662 config,
663 has_credentials,
664 enabled: d.enabled,
665 created_at: d.created_at,
666 updated_at: d.updated_at,
667 }
668 }
669}
670
671#[derive(Debug, Deserialize)]
672pub struct BackupDestinationCreate {
673 pub name: String,
674 pub kind: String,
676 pub config: Option<Value>,
677 pub enabled: Option<bool>,
678}
679
680#[derive(Debug, Deserialize, Default)]
681pub struct BackupDestinationUpdate {
682 pub name: Option<String>,
683 pub kind: Option<String>,
684 pub config: Option<Value>,
685 pub enabled: Option<bool>,
686}
687
688#[derive(Debug, Clone, Serialize)]
690pub struct BackupTestResult {
691 pub ok: bool,
692 pub error: Option<String>,
693 pub latency_ms: i64,
694}
695
696#[derive(Debug, Clone, Serialize, FromRow)]
698pub struct BackupPolicy {
699 pub id: String,
700 pub name: String,
701 pub destination_id: String,
702 pub camera_ids: Json<Value>,
704 pub incident_lock_only: bool,
705 pub schedule_interval_s: i64,
706 pub lookback_hours: i64,
707 pub last_run_at: Option<DateTime<Utc>>,
708 pub last_job_id: Option<String>,
709 pub enabled: bool,
710 pub created_at: DateTime<Utc>,
711 pub updated_at: DateTime<Utc>,
712}
713
714#[derive(Debug, Deserialize)]
715pub struct BackupPolicyCreate {
716 pub name: String,
717 pub destination_id: String,
718 pub camera_ids: Option<Value>,
719 pub incident_lock_only: Option<bool>,
720 pub schedule_interval_s: Option<i64>,
721 pub lookback_hours: Option<i64>,
722 pub enabled: Option<bool>,
723}
724
725#[derive(Debug, Deserialize, Default)]
726pub struct BackupPolicyUpdate {
727 pub name: Option<String>,
728 pub destination_id: Option<String>,
729 pub camera_ids: Option<Value>,
730 pub incident_lock_only: Option<bool>,
731 pub schedule_interval_s: Option<i64>,
732 pub lookback_hours: Option<i64>,
733 pub enabled: Option<bool>,
734}
735
736#[derive(Debug, Clone, Serialize, FromRow)]
738pub struct BackupJob {
739 pub id: String,
740 pub policy_id: Option<String>,
741 pub destination_id: Option<String>,
742 pub kind: String,
744 pub camera_ids: Json<Value>,
745 pub from_time: Option<DateTime<Utc>>,
746 pub to_time: Option<DateTime<Utc>>,
747 pub incident_lock_only: bool,
748 pub status: String,
750 pub files_total: i64,
751 pub files_copied: i64,
752 pub bytes_copied: i64,
753 pub error: Option<String>,
754 pub output_path: Option<String>,
756 pub output_url: Option<String>,
758 pub started_at: Option<DateTime<Utc>>,
759 pub finished_at: Option<DateTime<Utc>>,
760 pub created_at: DateTime<Utc>,
761}
762
763#[derive(Debug, Clone, Serialize, FromRow)]
769pub struct CameraOnvif {
770 pub camera_id: String,
771 pub device_url: String,
772 pub manufacturer: Option<String>,
773 pub model: Option<String>,
774 pub firmware_version: Option<String>,
775 pub serial_number: Option<String>,
776 pub hardware_id: Option<String>,
777 pub scopes: Json<Value>,
778 pub media_url: Option<String>,
779 pub ptz_url: Option<String>,
780 pub profile_token: Option<String>,
781 pub ptz_node_token: Option<String>,
782 pub ptz_enabled: bool,
783 pub probed_at: DateTime<Utc>,
784}
785
786#[derive(Debug, Clone, Serialize, FromRow)]
788pub struct PtzPreset {
789 pub id: String,
790 pub camera_id: String,
791 pub token: String,
792 pub name: Option<String>,
793 pub fetched_at: DateTime<Utc>,
794}
795
796#[derive(Debug, Clone, FromRow, Serialize)]
802pub struct CameraIsapi {
803 pub camera_id: String,
804 pub device_name: Option<String>,
805 pub model: Option<String>,
806 pub firmware_version: Option<String>,
807 pub serial_number: Option<String>,
808 pub onvif_enabled: bool,
809 pub onvif_user_created: bool,
810 pub time_mode: Option<String>,
811 pub ntp_server: Option<String>,
812 pub fetched_at: DateTime<Utc>,
813}
814
815fn de_field_present<'de, D>(deserializer: D) -> Result<Option<Option<String>>, D::Error>
821where
822 D: serde::Deserializer<'de>,
823{
824 Ok(Some(Option::<String>::deserialize(deserializer)?))
825}
826
827pub fn mask_webhook_url(url: &str) -> Option<String> {
831 let url = url.trim();
832 if url.is_empty() {
833 return None;
834 }
835 match url.split_once("://") {
836 Some((scheme, rest)) => {
837 let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
838 let authority = &rest[..authority_end];
839 if authority_end < rest.len() {
840 Some(format!("{scheme}://{authority}/…"))
841 } else {
842 Some(format!("{scheme}://{authority}"))
843 }
844 }
845 None => Some("…".to_string()),
846 }
847}
848
849#[derive(Debug, Clone, FromRow)]
856pub struct WebhookSubscription {
857 pub id: String,
858 pub name: String,
859 pub url: String,
860 pub event_types: Json<Vec<String>>,
861 pub min_severity: String,
862 pub secret: Option<String>,
863 pub enabled: bool,
864 pub cursor_at: Option<DateTime<Utc>>,
865 pub created_at: DateTime<Utc>,
866 pub updated_at: DateTime<Utc>,
867}
868
869#[derive(Debug, Clone, Serialize)]
871pub struct WebhookSubscriptionView {
872 pub id: String,
873 pub name: String,
874 pub url: String,
875 pub event_types: Vec<String>,
876 pub min_severity: String,
877 pub has_secret: bool,
879 pub enabled: bool,
880 pub cursor_at: Option<DateTime<Utc>>,
881 pub created_at: DateTime<Utc>,
882 pub updated_at: DateTime<Utc>,
883}
884
885impl From<WebhookSubscription> for WebhookSubscriptionView {
886 fn from(s: WebhookSubscription) -> Self {
887 WebhookSubscriptionView {
888 id: s.id,
889 name: s.name,
890 url: s.url,
891 event_types: s.event_types.0,
892 min_severity: s.min_severity,
893 has_secret: s.secret.as_deref().map(|v| !v.is_empty()).unwrap_or(false),
894 enabled: s.enabled,
895 cursor_at: s.cursor_at,
896 created_at: s.created_at,
897 updated_at: s.updated_at,
898 }
899 }
900}
901
902#[derive(Debug, Deserialize)]
903pub struct WebhookSubscriptionCreate {
904 pub name: String,
905 pub url: String,
906 pub event_types: Option<Vec<String>>,
908 pub min_severity: Option<String>,
910 pub secret: Option<String>,
912 pub enabled: Option<bool>,
913}
914
915#[derive(Debug, Deserialize, Default)]
919pub struct WebhookSubscriptionUpdate {
920 pub name: Option<String>,
921 pub url: Option<String>,
922 pub event_types: Option<Vec<String>>,
923 pub min_severity: Option<String>,
924 #[serde(default, deserialize_with = "de_field_present")]
925 pub secret: Option<Option<String>>,
926 pub enabled: Option<bool>,
927}
928
929#[derive(Debug, Clone, Serialize, FromRow)]
931pub struct WebhookDelivery {
932 pub id: String,
933 pub subscription_id: String,
934 pub event_id: Option<String>,
935 pub event_type: Option<String>,
936 pub status: String,
937 pub attempts: i64,
938 pub response_code: Option<i64>,
939 pub error: Option<String>,
940 pub created_at: DateTime<Utc>,
941 pub delivered_at: Option<DateTime<Utc>>,
942}
943
944#[derive(Debug, Deserialize)]
946pub struct ArchiveExportRequest {
947 #[serde(default)]
949 pub camera_ids: Vec<String>,
950 pub from: Option<String>,
951 pub to: Option<String>,
952 pub incident_lock_only: Option<bool>,
953 pub trim: Option<bool>,
955}
956
957#[cfg(test)]
958mod tests {
959 use super::*;
960
961 #[test]
962 fn mask_webhook_url_hides_path_and_token() {
963 assert_eq!(
965 mask_webhook_url("https://hooks.slack.com/services/T000/B000/XXXXSECRET"),
966 Some("https://hooks.slack.com/…".to_string())
967 );
968 assert_eq!(
969 mask_webhook_url("https://example.com:8443/alert?token=abc"),
970 Some("https://example.com:8443/…".to_string())
971 );
972 assert_eq!(
974 mask_webhook_url("https://example.com"),
975 Some("https://example.com".to_string())
976 );
977 assert_eq!(mask_webhook_url(" "), None);
979 assert_eq!(mask_webhook_url("not-a-url"), Some("…".to_string()));
980 }
981
982 #[test]
983 fn webhook_update_secret_is_three_state() {
984 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"enabled": true}"#).unwrap();
986 assert!(u.secret.is_none());
987 assert_eq!(u.enabled, Some(true));
988 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"secret": null}"#).unwrap();
990 assert_eq!(u.secret, Some(None));
991 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"secret": "s3cr3t"}"#).unwrap();
993 assert_eq!(u.secret, Some(Some("s3cr3t".to_string())));
994 }
995}