1#![allow(clippy::struct_excessive_bools)]
2
3use std::{collections::BTreeMap, fmt};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Clone, Default, Deserialize, PartialEq, Serialize)]
10#[serde(default, rename_all = "camelCase")]
11pub struct WebSettings {
12 pub web_listen: String,
14 pub web_domain: String,
16 pub web_port: u16,
18 pub web_cert_file: String,
20 pub web_key_file: String,
22 pub web_base_path: String,
24 pub session_max_age: u32,
26 #[serde(rename = "trustedProxyCIDRs")]
28 pub trusted_proxy_cidrs: String,
29 pub panel_outbound: String,
31}
32
33impl fmt::Debug for WebSettings {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 formatter
36 .debug_struct("WebSettings")
37 .field("web_listen", &self.web_listen)
38 .field("web_domain", &self.web_domain)
39 .field("web_port", &self.web_port)
40 .field("web_cert_file", &self.web_cert_file)
41 .field("web_key_file", &"[REDACTED]")
42 .field("web_base_path", &"[REDACTED]")
43 .field("session_max_age", &self.session_max_age)
44 .field("trusted_proxy_cidrs", &self.trusted_proxy_cidrs)
45 .field("panel_outbound", &"[REDACTED]")
46 .finish_non_exhaustive()
47 }
48}
49
50#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
52#[serde(default, rename_all = "camelCase")]
53pub struct DisplaySettings {
54 pub page_size: u32,
56 pub expire_diff: u32,
58 pub traffic_diff: u8,
60 pub remark_template: String,
62 pub sub_show_identity_on_all_links: bool,
64 pub datepicker: String,
66}
67
68#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
70#[serde(default, rename_all = "camelCase")]
71pub struct TelegramSettings {
72 pub tg_bot_enable: bool,
74 pub tg_bot_token: String,
76 pub tg_bot_proxy: String,
78 #[serde(rename = "tgBotAPIServer")]
80 pub tg_bot_api_server: String,
81 pub tg_bot_chat_id: String,
83 pub tg_run_time: String,
85 pub tg_bot_backup: bool,
87 pub tg_cpu: u8,
89 pub tg_memory: u8,
91 pub tg_lang: String,
93 pub tg_enabled_events: String,
95}
96
97impl fmt::Debug for TelegramSettings {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 formatter
100 .debug_struct("TelegramSettings")
101 .field("tg_bot_enable", &self.tg_bot_enable)
102 .field("tg_bot_token", &"[REDACTED]")
103 .field("tg_bot_proxy", &"[REDACTED]")
104 .field("tg_bot_api_server", &self.tg_bot_api_server)
105 .field("tg_bot_chat_id", &self.tg_bot_chat_id)
106 .field("tg_run_time", &self.tg_run_time)
107 .field("tg_bot_backup", &self.tg_bot_backup)
108 .field("tg_cpu", &self.tg_cpu)
109 .field("tg_memory", &self.tg_memory)
110 .field("tg_lang", &self.tg_lang)
111 .field("tg_enabled_events", &self.tg_enabled_events)
112 .finish()
113 }
114}
115
116#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
118#[serde(default, rename_all = "camelCase")]
119pub struct SmtpSettings {
120 pub smtp_enable: bool,
122 pub smtp_host: String,
124 pub smtp_port: u16,
126 pub smtp_username: String,
128 pub smtp_password: String,
130 pub smtp_from: String,
132 pub smtp_from_name: String,
134 pub smtp_to: String,
136 pub smtp_encryption_type: String,
138 pub smtp_enabled_events: String,
140 pub smtp_cpu: u8,
142 pub smtp_memory: u8,
144}
145
146impl fmt::Debug for SmtpSettings {
147 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148 formatter
149 .debug_struct("SmtpSettings")
150 .field("smtp_enable", &self.smtp_enable)
151 .field("smtp_host", &self.smtp_host)
152 .field("smtp_port", &self.smtp_port)
153 .field("smtp_username", &self.smtp_username)
154 .field("smtp_password", &"[REDACTED]")
155 .field("smtp_from", &self.smtp_from)
156 .field("smtp_from_name", &self.smtp_from_name)
157 .field("smtp_to", &self.smtp_to)
158 .field("smtp_encryption_type", &self.smtp_encryption_type)
159 .field("smtp_enabled_events", &self.smtp_enabled_events)
160 .field("smtp_cpu", &self.smtp_cpu)
161 .field("smtp_memory", &self.smtp_memory)
162 .finish()
163 }
164}
165
166#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
168#[serde(default, rename_all = "camelCase")]
169pub struct SecuritySettings {
170 pub reality_scan_candidates: String,
172 pub outbound_down_threshold: u8,
174 pub ip_limit_allowlist: String,
176 pub time_location: String,
178 pub two_factor_enable: bool,
180 pub two_factor_token: String,
182 pub warp_update_interval: u32,
184}
185
186impl fmt::Debug for SecuritySettings {
187 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 formatter
190 .debug_struct("SecuritySettings")
191 .field("reality_scan_candidates", &self.reality_scan_candidates)
192 .field("outbound_down_threshold", &self.outbound_down_threshold)
193 .field("ip_limit_allowlist", &self.ip_limit_allowlist)
194 .field("time_location", &self.time_location)
195 .field("two_factor_enable", &self.two_factor_enable)
196 .field("two_factor_token", &"[REDACTED]")
197 .field("warp_update_interval", &self.warp_update_interval)
198 .finish()
199 }
200}
201
202#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
204#[serde(default, rename_all = "camelCase")]
205pub struct SubscriptionSettings {
206 pub happ_link_enable: bool,
208 pub sub_info_node_enable: bool,
210 pub sub_calendar_expire_inclusive: bool,
212 pub sub_expired_template: String,
214 pub sub_traffic_depleted_template: String,
216 #[serde(flatten)]
218 pub happ: HappSettings,
219 pub sub_enable: bool,
221 pub sub_json_enable: bool,
223 pub sub_json_auto_detect: bool,
225 pub sub_json_always_array: bool,
227 pub sub_json_user_agent_regex: String,
229 pub sub_clash_auto_detect: bool,
231 pub sub_clash_user_agent_regex: String,
233 pub sub_title: String,
235 pub sub_support_url: String,
237 pub sub_profile_mode: String,
239 pub sub_profile_url: String,
241 pub sub_announce: String,
243 pub sub_enable_routing: bool,
245 pub sub_routing_rules: String,
247 pub sub_incy_enable_routing: bool,
249 pub sub_incy_routing_rules: String,
251 pub sub_listen: String,
253 pub sub_port: u16,
255 pub sub_path: String,
257 pub sub_domain: String,
259 pub sub_cert_file: String,
261 pub sub_key_file: String,
263 pub sub_updates: u32,
265 pub external_traffic_inform_enable: bool,
267 #[serde(rename = "externalTrafficInformURI")]
269 pub external_traffic_inform_uri: String,
270 pub restart_xray_on_client_disable: bool,
272 pub sub_encrypt: bool,
274 #[serde(rename = "subURI")]
276 pub sub_uri: String,
277 pub sub_json_path: String,
279 #[serde(rename = "subJsonURI")]
281 pub sub_json_uri: String,
282 pub sub_clash_enable: bool,
284 pub sub_clash_path: String,
286 #[serde(rename = "subClashURI")]
288 pub sub_clash_uri: String,
289 pub sub_clash_enable_routing: bool,
291 pub sub_clash_rules: String,
293 pub sub_json_mux: String,
295 pub sub_json_rules: String,
297 pub sub_json_routing_rules: String,
299 pub sub_json_dns: String,
301 pub sub_json_final_mask: String,
303 pub sub_json_observatory: String,
305 pub sub_theme_dir: String,
307 pub sub_hide_settings: bool,
309}
310
311impl fmt::Debug for SubscriptionSettings {
312 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
313 formatter
314 .debug_struct("SubscriptionSettings")
315 .field("sub_enable", &self.sub_enable)
316 .field("sub_json_enable", &self.sub_json_enable)
317 .field("sub_json_auto_detect", &self.sub_json_auto_detect)
318 .field("sub_json_always_array", &self.sub_json_always_array)
319 .field("sub_clash_auto_detect", &self.sub_clash_auto_detect)
320 .field("sub_title", &self.sub_title)
321 .field("sub_listen", &self.sub_listen)
322 .field("sub_port", &self.sub_port)
323 .field("sub_domain", &self.sub_domain)
324 .field("sub_updates", &self.sub_updates)
325 .field(
326 "external_traffic_inform_enable",
327 &self.external_traffic_inform_enable,
328 )
329 .field(
330 "restart_xray_on_client_disable",
331 &self.restart_xray_on_client_disable,
332 )
333 .field("sub_encrypt", &self.sub_encrypt)
334 .field("sub_clash_enable", &self.sub_clash_enable)
335 .field("sub_clash_enable_routing", &self.sub_clash_enable_routing)
336 .field("sub_hide_settings", &self.sub_hide_settings)
337 .field("paths_uris_rules_and_key_material", &"[REDACTED]")
338 .finish_non_exhaustive()
339 }
340}
341
342#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
344#[serde(default, rename_all = "camelCase")]
345pub struct LdapSettings {
346 pub ldap_enable: bool,
348 pub ldap_host: String,
350 pub ldap_port: u16,
352 #[serde(rename = "ldapUseTLS")]
354 pub ldap_use_tls: bool,
355 pub ldap_insecure_skip_verify: bool,
357 #[serde(rename = "ldapBindDN")]
359 pub ldap_bind_dn: String,
360 pub ldap_password: String,
362 #[serde(rename = "ldapBaseDN")]
364 pub ldap_base_dn: String,
365 pub ldap_user_filter: String,
367 pub ldap_user_attr: String,
369 pub ldap_vless_field: String,
371 pub ldap_sync_cron: String,
373 pub ldap_flag_field: String,
375 pub ldap_truthy_values: String,
377 pub ldap_invert_flag: bool,
379 pub ldap_inbound_tags: String,
381 pub ldap_auto_create: bool,
383 pub ldap_auto_delete: bool,
385 #[serde(rename = "ldapDefaultTotalGB")]
387 pub ldap_default_total_gb: u64,
388 pub ldap_default_expiry_days: u32,
390 #[serde(rename = "ldapDefaultLimitIP")]
392 pub ldap_default_limit_ip: u32,
393}
394
395impl fmt::Debug for LdapSettings {
396 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
397 formatter
398 .debug_struct("LdapSettings")
399 .field("ldap_enable", &self.ldap_enable)
400 .field("ldap_host", &self.ldap_host)
401 .field("ldap_port", &self.ldap_port)
402 .field("ldap_use_tls", &self.ldap_use_tls)
403 .field("ldap_insecure_skip_verify", &self.ldap_insecure_skip_verify)
404 .field("ldap_bind_dn", &self.ldap_bind_dn)
405 .field("ldap_password", &"[REDACTED]")
406 .field("ldap_base_dn", &self.ldap_base_dn)
407 .field("ldap_user_filter", &self.ldap_user_filter)
408 .field("ldap_user_attr", &self.ldap_user_attr)
409 .field("ldap_vless_field", &self.ldap_vless_field)
410 .field("ldap_sync_cron", &self.ldap_sync_cron)
411 .field("ldap_flag_field", &self.ldap_flag_field)
412 .field("ldap_truthy_values", &self.ldap_truthy_values)
413 .field("ldap_invert_flag", &self.ldap_invert_flag)
414 .field("ldap_inbound_tags", &self.ldap_inbound_tags)
415 .field("ldap_auto_create", &self.ldap_auto_create)
416 .field("ldap_auto_delete", &self.ldap_auto_delete)
417 .field("ldap_default_total_gb", &self.ldap_default_total_gb)
418 .field("ldap_default_expiry_days", &self.ldap_default_expiry_days)
419 .field("ldap_default_limit_ip", &self.ldap_default_limit_ip)
420 .finish()
421 }
422}
423
424#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
426#[serde(default, rename_all = "camelCase")]
427pub struct DiscordSettings {
428 pub discord_admin_ids: String,
430 pub discord_bot_backup: bool,
432 pub discord_bot_enable: bool,
434 pub discord_bot_token: String,
436 pub discord_channel_id: String,
438 pub discord_cpu: u8,
440 pub discord_enabled_events: String,
442 pub discord_lang: String,
444 pub discord_memory: u8,
446 pub discord_run_time: String,
448}
449
450impl fmt::Debug for DiscordSettings {
451 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
453 formatter
454 .debug_struct("DiscordSettings")
455 .field("settings", &"[REDACTED]")
456 .finish_non_exhaustive()
457 }
458}
459
460#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
462#[serde(default, rename_all = "camelCase")]
463pub struct HappSettings {
464 pub sub_happ_always_hwid: bool,
466 pub sub_happ_auto_connect: bool,
468 pub sub_happ_auto_connect_type: String,
470 pub sub_happ_auto_detect: bool,
472 pub sub_happ_color_profile: String,
474 pub sub_happ_exclude_apns: bool,
476 pub sub_happ_exclude_routes: String,
478 pub sub_happ_fallback_url: String,
480 pub sub_happ_new_url: String,
482 pub sub_happ_no_limit: bool,
484 pub sub_happ_notification_expire: bool,
486 pub sub_happ_per_app_list: String,
488 pub sub_happ_per_app_mode: String,
490 pub sub_happ_ping_type: String,
492 pub sub_happ_provider_id: String,
494 pub sub_happ_sub_expire: bool,
496 pub sub_happ_sub_expire_button_link: String,
498 pub sub_happ_sub_info_button_link: String,
500 pub sub_happ_sub_info_button_text: String,
502 pub sub_happ_sub_info_color: String,
504 pub sub_happ_sub_info_text: String,
506 pub sub_happ_tun_mode: String,
508 pub sub_happ_tun_type: String,
510}
511
512impl fmt::Debug for HappSettings {
513 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
515 formatter
516 .debug_struct("HappSettings")
517 .field("settings", &"[REDACTED]")
518 .finish_non_exhaustive()
519 }
520}
521
522#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
524#[serde(default)]
525pub struct PanelSettings {
526 #[serde(flatten)]
528 pub web: WebSettings,
529 #[serde(flatten)]
531 pub display: DisplaySettings,
532 #[serde(flatten)]
534 pub telegram: TelegramSettings,
535 #[serde(flatten)]
537 pub smtp: SmtpSettings,
538 #[serde(flatten)]
540 pub discord: DiscordSettings,
541 #[serde(flatten)]
543 pub security: SecuritySettings,
544 #[serde(flatten)]
546 pub subscriptions: SubscriptionSettings,
547 #[serde(flatten)]
549 pub ldap: LdapSettings,
550}
551
552#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
554#[serde(default, rename_all = "camelCase")]
555pub struct PanelSettingsView {
556 #[serde(flatten)]
558 pub settings: PanelSettings,
559 pub has_tg_bot_token: bool,
561 pub has_two_factor_token: bool,
563 pub has_ldap_password: bool,
565 pub has_api_token: bool,
567 pub has_warp_secret: bool,
569 pub has_nord_secret: bool,
571 pub has_discord_bot_token: bool,
573 pub has_smtp_password: bool,
575}
576
577#[derive(Clone, Debug, Default, Serialize)]
579#[serde(rename_all = "camelCase")]
580pub struct PanelSettingsUpdate {
581 #[serde(flatten)]
583 pub settings: PanelSettings,
584 pub two_factor_code: String,
586 pub clear_discord_bot_token: bool,
588 pub clear_tg_bot_token: bool,
590 pub clear_ldap_password: bool,
592 pub clear_smtp_password: bool,
594}
595
596impl PanelSettingsUpdate {
597 pub fn new(settings: PanelSettings) -> Self {
599 Self {
600 settings,
601 ..Self::default()
602 }
603 }
604}
605
606#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
608#[serde(default, rename_all = "camelCase")]
609pub struct EffectiveDefaults {
610 pub expire_diff: i64,
612 pub traffic_diff: i64,
614 pub page_size: i64,
616 pub default_cert: String,
618 pub default_key: String,
620 pub tg_bot_enable: bool,
622 pub sub_theme_dir: String,
624 pub sub_enable: bool,
626 pub sub_json_enable: bool,
628 pub sub_clash_enable: bool,
630 pub sub_title: String,
632 #[serde(rename = "subURI")]
634 pub sub_uri: String,
635 #[serde(rename = "subJsonURI")]
637 pub sub_json_uri: String,
638 #[serde(rename = "subClashURI")]
640 pub sub_clash_uri: String,
641 pub datepicker: String,
643 pub ip_limit_enable: bool,
645 pub access_log_enable: bool,
647 pub web_domain: String,
649 pub sub_domain: String,
651 pub dev_channel_enable: bool,
653 pub is_dev_build: bool,
655}
656
657pub type FactoryDefaults = BTreeMap<String, String>;
659
660#[derive(Clone, Serialize)]
662#[serde(rename_all = "camelCase")]
663pub struct UserCredentialsUpdate {
664 pub old_username: String,
666 pub old_password: String,
668 pub new_username: String,
670 pub new_password: String,
672 pub two_factor_code: String,
674}
675
676impl fmt::Debug for UserCredentialsUpdate {
677 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
678 formatter
679 .debug_struct("UserCredentialsUpdate")
680 .field("old_username", &self.old_username)
681 .field("old_password", &"[REDACTED]")
682 .field("new_username", &self.new_username)
683 .field("new_password", &"[REDACTED]")
684 .field("two_factor_code", &"[REDACTED]")
685 .finish()
686 }
687}
688
689#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
691#[serde(rename_all = "camelCase")]
692pub struct ApiTokenMetadata {
693 pub id: i64,
695 pub name: String,
697 pub enabled: bool,
699 pub created_at: i64,
701 #[serde(default)]
703 pub scope: ApiTokenScope,
704 #[serde(default)]
706 pub expires_at: i64,
707}
708
709#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
711#[serde(rename_all = "kebab-case")]
712#[non_exhaustive]
713pub enum ApiTokenScope {
714 #[default]
716 Admin,
717 Monitor,
719 NodeSync,
721}
722
723impl ApiTokenScope {
724 pub(crate) const fn as_str(self) -> &'static str {
725 match self {
726 Self::Admin => "admin",
727 Self::Monitor => "monitor",
728 Self::NodeSync => "node-sync",
729 }
730 }
731}
732
733#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
735#[serde(rename_all = "camelCase")]
736pub struct ApiTokenCreateRequest {
737 pub name: String,
739 #[serde(default)]
741 pub scope: ApiTokenScope,
742 #[serde(default)]
744 pub expires_at: i64,
745}
746
747impl ApiTokenCreateRequest {
748 pub fn new(name: impl Into<String>) -> Self {
750 Self {
751 name: name.into(),
752 scope: ApiTokenScope::Admin,
753 expires_at: 0,
754 }
755 }
756}
757
758#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
760#[serde(rename_all = "camelCase")]
761pub struct CreatedApiToken {
762 pub id: i64,
764 pub name: String,
766 pub token: String,
768 pub enabled: bool,
770 pub created_at: i64,
772 pub scope: ApiTokenScope,
774 pub expires_at: i64,
776}
777
778impl fmt::Debug for CreatedApiToken {
779 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
780 formatter
781 .debug_struct("CreatedApiToken")
782 .field("id", &self.id)
783 .field("name", &self.name)
784 .field("token", &"[REDACTED]")
785 .field("enabled", &self.enabled)
786 .field("created_at", &self.created_at)
787 .field("scope", &self.scope)
788 .field("expires_at", &self.expires_at)
789 .finish()
790 }
791}
792
793#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
795#[serde(default, rename_all = "camelCase")]
796pub struct GeoFile {
797 pub name: String,
799 pub kind: String,
801 pub size: i64,
803 pub modified_at: i64,
805 pub categories: i32,
807 pub error: String,
809}
810
811#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
813#[serde(default)]
814pub struct GeoCategory {
815 pub code: String,
817 pub entries: i32,
819 pub attributes: Vec<String>,
821}
822
823#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
825#[serde(default)]
826pub struct GeoCategoryPage {
827 pub items: Vec<GeoCategory>,
829 pub total: i32,
831}
832
833#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
835#[serde(default)]
836pub struct GeoEntry {
837 pub kind: String,
839 pub value: String,
841}
842
843#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
845#[serde(default)]
846pub struct GeoEntryPage {
847 pub items: Vec<GeoEntry>,
849 pub total: i32,
851}
852
853#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
855#[serde(default)]
856pub struct GeodataTokenIssue {
857 pub token: String,
859 pub reason: String,
861 pub file: String,
863 pub code: String,
865}
866
867#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
869#[serde(default)]
870pub struct PiaCountry {
871 pub code: String,
873}
874
875#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
877#[serde(default)]
878pub struct PiaRegion {
879 pub id: String,
881 pub name: String,
883}
884
885#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
887#[serde(default, rename_all = "camelCase")]
888pub struct PiaServer {
889 pub hostname: String,
891 pub ip: String,
893 pub region_id: String,
895 pub region_name: String,
897}
898
899#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
901#[serde(default)]
902pub struct PiaServers {
903 pub regions: Vec<PiaRegion>,
905 pub servers: Vec<PiaServer>,
907}
908
909#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
911#[serde(default, rename_all = "camelCase")]
912pub struct PiaAccount {
913 pub username: String,
915 pub account_hint: String,
917}
918
919#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
921#[serde(default, rename_all = "camelCase")]
922pub struct PiaKey {
923 pub tag: String,
925 pub hostname: String,
927 pub secret_key: String,
929 pub address: String,
931 pub public_key: String,
933 pub endpoint: String,
935}
936
937impl fmt::Debug for PiaKey {
938 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
939 formatter
940 .debug_struct("PiaKey")
941 .field("tag", &self.tag)
942 .field("hostname", &self.hostname)
943 .field("secret_key", &"[REDACTED]")
944 .field("address", &self.address)
945 .field("public_key", &self.public_key)
946 .field("endpoint", &self.endpoint)
947 .finish()
948 }
949}
950
951#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
953pub struct SmtpTestResult {
954 pub success: bool,
956 #[serde(default)]
958 pub stage: String,
959 #[serde(default, rename = "msg")]
961 pub message: String,
962}
963
964#[derive(Clone, Deserialize, PartialEq, Serialize)]
966#[serde(transparent)]
967pub struct SensitivePayload(String);
968
969impl SensitivePayload {
970 pub fn as_str(&self) -> &str {
972 &self.0
973 }
974
975 pub fn json(&self) -> serde_json::Result<Value> {
981 serde_json::from_str(&self.0)
982 }
983}
984
985impl fmt::Debug for SensitivePayload {
986 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
987 formatter
988 .debug_tuple("SensitivePayload")
989 .field(&"[REDACTED]")
990 .finish()
991 }
992}
993
994#[derive(Clone, Serialize)]
996#[serde(rename_all = "camelCase")]
997pub struct WarpRegistration {
998 pub private_key: String,
1000 pub public_key: String,
1002}
1003
1004impl fmt::Debug for WarpRegistration {
1005 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1006 formatter
1007 .debug_struct("WarpRegistration")
1008 .field("private_key", &"[REDACTED]")
1009 .field("public_key", &self.public_key)
1010 .finish()
1011 }
1012}
1013
1014#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1016pub struct GeodataSource {
1017 pub url: String,
1019 pub file: String,
1021}
1022
1023#[derive(Clone, Deserialize, PartialEq, Serialize)]
1025#[serde(default, rename_all = "camelCase")]
1026pub struct XraySettingsSnapshot {
1027 pub xray_setting: crate::XrayConfig,
1029 pub geodata_sources: Vec<GeodataSource>,
1031 pub inbound_tags: Vec<String>,
1033 pub client_reverse_tags: Vec<String>,
1035 pub outbound_test_url: String,
1037 pub subscription_outbounds: Vec<Value>,
1039 pub subscription_outbound_tags: Vec<String>,
1041}
1042
1043impl Default for XraySettingsSnapshot {
1044 fn default() -> Self {
1046 Self {
1047 xray_setting: crate::XrayConfig::from(Value::Object(serde_json::Map::new())),
1048 inbound_tags: Vec::new(),
1049 geodata_sources: Vec::new(),
1050 client_reverse_tags: Vec::new(),
1051 outbound_test_url: String::new(),
1052 subscription_outbounds: Vec::new(),
1053 subscription_outbound_tags: Vec::new(),
1054 }
1055 }
1056}
1057
1058impl fmt::Debug for XraySettingsSnapshot {
1059 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1061 formatter
1062 .debug_struct("XraySettingsSnapshot")
1063 .field("geodata_sources", &self.geodata_sources)
1064 .field("xray_setting", &"[REDACTED]")
1065 .field("inbound_tags", &self.inbound_tags)
1066 .field("client_reverse_tags", &self.client_reverse_tags)
1067 .field("outbound_test_url", &self.outbound_test_url)
1068 .field("subscription_outbounds", &"[REDACTED]")
1069 .field(
1070 "subscription_outbound_tags",
1071 &self.subscription_outbound_tags,
1072 )
1073 .finish()
1074 }
1075}
1076
1077#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1079#[serde(default)]
1080pub struct OutboundTraffic {
1081 pub id: i64,
1083 pub tag: String,
1085 pub up: i64,
1087 pub down: i64,
1089 pub total: i64,
1091}
1092
1093#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
1095#[non_exhaustive]
1096pub enum OutboundTestMode {
1097 #[default]
1099 Http,
1100 Tcp,
1102 Real,
1104}
1105
1106impl OutboundTestMode {
1107 pub(crate) const fn as_str(self) -> &'static str {
1108 match self {
1109 Self::Http => "",
1110 Self::Tcp => "tcp",
1111 Self::Real => "real",
1112 }
1113 }
1114}
1115
1116#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1118#[serde(default)]
1119pub struct TestEndpointResult {
1120 pub address: String,
1122 pub success: bool,
1124 pub delay: i64,
1126 pub error: String,
1128}
1129
1130#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1132#[serde(default)]
1133pub struct TestEgressResult {
1134 pub ipv4: String,
1136 pub ipv6: String,
1138 pub country: String,
1140 pub warp: String,
1142}
1143
1144#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1146#[serde(default, rename_all = "camelCase")]
1147pub struct OutboundTestResult {
1148 pub tag: String,
1150 pub success: bool,
1152 pub delay: i64,
1154 pub error: String,
1156 pub mode: String,
1158 pub http_status: i32,
1160 pub connect_ms: i64,
1162 #[serde(rename = "tlsMs")]
1164 pub tls_ms: i64,
1165 #[serde(rename = "ttfbMs")]
1167 pub ttfb_ms: i64,
1168 pub endpoints: Vec<TestEndpointResult>,
1170 pub egress: Option<TestEgressResult>,
1172}
1173
1174#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1176#[serde(default)]
1177pub struct BalancerStatus {
1178 pub tag: String,
1180 pub running: bool,
1182 #[serde(rename = "override")]
1184 pub override_target: String,
1185 pub selected: Vec<String>,
1187}
1188
1189#[derive(Clone, Debug, Default, Serialize)]
1191#[serde(rename_all = "camelCase")]
1192pub struct RouteTestRequest {
1193 pub inbound_tag: String,
1195 pub domain: String,
1197 pub ip: String,
1199 pub port: u16,
1201 pub network: String,
1203 pub protocol: String,
1205 pub email: String,
1207}
1208
1209#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1211#[serde(default, rename_all = "camelCase")]
1212pub struct RouteTestResult {
1213 pub matched: bool,
1215 pub outbound_tag: String,
1217 pub group_tags: Vec<String>,
1219}
1220
1221#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
1223#[serde(default, rename_all = "camelCase")]
1224pub struct OutboundSubscription {
1225 pub id: i64,
1227 pub remark: String,
1229 pub url: String,
1231 pub enabled: bool,
1233 pub allow_private: bool,
1235 pub allow_insecure: bool,
1237 pub user_agent: String,
1239 pub tag_prefix: String,
1241 pub update_interval: i32,
1243 pub priority: i32,
1245 pub prepend: bool,
1247 pub last_updated: i64,
1249 pub last_error: String,
1251 pub last_fetched_outbounds: String,
1253 pub created_at: i64,
1255 pub updated_at: i64,
1257 pub outbound_count: i32,
1259}
1260
1261impl fmt::Debug for OutboundSubscription {
1262 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1264 formatter
1265 .debug_struct("OutboundSubscription")
1266 .field("user_agent", &"[REDACTED]")
1267 .field("id", &self.id)
1268 .field("remark", &self.remark)
1269 .field("url", &"[REDACTED]")
1270 .field("enabled", &self.enabled)
1271 .field("allow_private", &self.allow_private)
1272 .field("allow_insecure", &self.allow_insecure)
1273 .field("tag_prefix", &self.tag_prefix)
1274 .field("update_interval", &self.update_interval)
1275 .field("priority", &self.priority)
1276 .field("prepend", &self.prepend)
1277 .field("last_updated", &self.last_updated)
1278 .field("last_error", &self.last_error)
1279 .field("last_fetched_outbounds", &"[REDACTED]")
1280 .field("created_at", &self.created_at)
1281 .field("updated_at", &self.updated_at)
1282 .field("outbound_count", &self.outbound_count)
1283 .finish()
1284 }
1285}
1286
1287#[derive(Clone, Serialize)]
1289#[serde(rename_all = "camelCase")]
1290pub struct OutboundSubscriptionInput {
1291 pub remark: String,
1293 pub url: String,
1295 pub user_agent: String,
1297 pub tag_prefix: String,
1299 pub enabled: bool,
1301 pub allow_private: bool,
1303 pub allow_insecure: bool,
1305 pub prepend: bool,
1307 pub update_interval: i32,
1309}
1310
1311impl OutboundSubscriptionInput {
1312 pub fn new(url: impl Into<String>) -> Self {
1314 Self {
1315 remark: String::new(),
1316 url: url.into(),
1317 tag_prefix: String::new(),
1318 user_agent: String::new(),
1319 enabled: true,
1320 allow_private: false,
1321 allow_insecure: false,
1322 prepend: false,
1323 update_interval: 600,
1324 }
1325 }
1326}
1327
1328impl fmt::Debug for OutboundSubscriptionInput {
1329 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1331 formatter
1332 .debug_struct("OutboundSubscriptionInput")
1333 .field("user_agent", &"[REDACTED]")
1334 .field("remark", &self.remark)
1335 .field("url", &"[REDACTED]")
1336 .field("tag_prefix", &self.tag_prefix)
1337 .field("enabled", &self.enabled)
1338 .field("allow_private", &self.allow_private)
1339 .field("allow_insecure", &self.allow_insecure)
1340 .field("prepend", &self.prepend)
1341 .field("update_interval", &self.update_interval)
1342 .finish()
1343 }
1344}
1345
1346#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1348#[non_exhaustive]
1349pub enum MoveDirection {
1350 Up,
1352 Down,
1354}
1355
1356impl MoveDirection {
1357 pub(crate) const fn as_str(self) -> &'static str {
1358 match self {
1359 Self::Up => "up",
1360 Self::Down => "down",
1361 }
1362 }
1363}
1364
1365#[derive(Clone, Default, Deserialize, PartialEq, Serialize)]
1367#[serde(transparent)]
1368pub struct OutboundDocuments(Vec<Value>);
1369
1370impl OutboundDocuments {
1371 pub fn as_slice(&self) -> &[Value] {
1373 &self.0
1374 }
1375 pub fn into_inner(self) -> Vec<Value> {
1377 self.0
1378 }
1379}
1380
1381impl fmt::Debug for OutboundDocuments {
1382 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1383 formatter
1384 .debug_tuple("OutboundDocuments")
1385 .field(&"[REDACTED]")
1386 .finish()
1387 }
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392 use super::*;
1393 use serde_json::json;
1394
1395 #[test]
1396 fn secret_bearing_models_redact_debug_output() {
1397 let credentials = UserCredentialsUpdate {
1398 old_username: "old-user".into(),
1399 old_password: "old-password-secret".into(),
1400 new_username: "new-user".into(),
1401 new_password: "new-password-secret".into(),
1402 two_factor_code: "123456-secret".into(),
1403 };
1404 let created = CreatedApiToken {
1405 id: 7,
1406 name: "automation".into(),
1407 token: "bearer-token-secret".into(),
1408 enabled: true,
1409 created_at: 10,
1410 scope: ApiTokenScope::Monitor,
1411 expires_at: 20,
1412 };
1413 let pia_key: PiaKey = serde_json::from_value(json!({
1414 "tag": "pia-de",
1415 "hostname": "de.example.com",
1416 "secretKey": "pia-private-secret",
1417 "address": "10.0.0.2/32",
1418 "publicKey": "public-key",
1419 "endpoint": "de.example.com:1337"
1420 }))
1421 .unwrap();
1422 let registration = WarpRegistration {
1423 private_key: "warp-private-secret".into(),
1424 public_key: "warp-public".into(),
1425 };
1426 let outbound: OutboundSubscription = serde_json::from_value(json!({
1427 "id": 9,
1428 "remark": "remote",
1429 "url": "https://user:subscription-secret@example.com/sub",
1430 "enabled": true,
1431 "lastFetchedOutbounds": "outbound-private-secret"
1432 }))
1433 .unwrap();
1434 let input = OutboundSubscriptionInput::new(
1435 "https://user:input-subscription-secret@example.com/sub",
1436 );
1437 let documents: OutboundDocuments =
1438 serde_json::from_value(json!([{"password": "document-secret"}])).unwrap();
1439 let snapshot = XraySettingsSnapshot {
1440 xray_setting: crate::XrayConfig::from(json!({"privateKey": "xray-secret"})),
1441 subscription_outbounds: vec![json!({"password": "snapshot-secret"})],
1442 ..XraySettingsSnapshot::default()
1443 };
1444
1445 let outputs = [
1446 format!("{credentials:?}"),
1447 format!("{created:?}"),
1448 format!("{pia_key:?}"),
1449 format!("{registration:?}"),
1450 format!("{outbound:?}"),
1451 format!("{input:?}"),
1452 format!("{documents:?}"),
1453 format!("{snapshot:?}"),
1454 ];
1455 for (output, secret) in outputs.iter().zip([
1456 "old-password-secret",
1457 "bearer-token-secret",
1458 "pia-private-secret",
1459 "warp-private-secret",
1460 "subscription-secret",
1461 "input-subscription-secret",
1462 "document-secret",
1463 "snapshot-secret",
1464 ]) {
1465 assert!(!output.contains(secret));
1466 assert!(output.contains("REDACTED"));
1467 }
1468 }
1469
1470 #[test]
1471 fn helpers_preserve_wire_values_and_owned_payloads() {
1472 assert_eq!(ApiTokenScope::Admin.as_str(), "admin");
1473 assert_eq!(ApiTokenScope::Monitor.as_str(), "monitor");
1474 assert_eq!(ApiTokenScope::NodeSync.as_str(), "node-sync");
1475 let request = ApiTokenCreateRequest::new("deploy");
1476 assert_eq!(request.name, "deploy");
1477 assert_eq!(request.scope, ApiTokenScope::Admin);
1478 assert_eq!(request.expires_at, 0);
1479
1480 let payload: SensitivePayload = serde_json::from_value(json!("{\"ok\":true}")).unwrap();
1481 assert_eq!(payload.as_str(), "{\"ok\":true}");
1482 assert_eq!(payload.json().unwrap(), json!({"ok": true}));
1483 assert!(
1484 serde_json::from_value::<SensitivePayload>(json!("not-json"))
1485 .unwrap()
1486 .json()
1487 .is_err()
1488 );
1489 assert!(!format!("{payload:?}").contains("ok"));
1490
1491 assert_eq!(OutboundTestMode::Http.as_str(), "");
1492 assert_eq!(OutboundTestMode::Tcp.as_str(), "tcp");
1493 assert_eq!(OutboundTestMode::Real.as_str(), "real");
1494 assert_eq!(MoveDirection::Up.as_str(), "up");
1495 assert_eq!(MoveDirection::Down.as_str(), "down");
1496
1497 let documents: OutboundDocuments =
1498 serde_json::from_value(json!([{"tag": "one"}, {"tag": "two"}])).unwrap();
1499 assert_eq!(documents.as_slice().len(), 2);
1500 assert_eq!(documents.into_inner()[1]["tag"], "two");
1501 }
1502}