1use std::{collections::HashMap, fmt};
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::{ClientTraffic, InboundProtocol};
6
7pub(crate) fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
8where
9 D: Deserializer<'de>,
10 T: Deserialize<'de> + Default,
11{
12 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
13}
14
15#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
17pub struct ClientReverse {
18 pub tag: String,
20}
21
22#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
28#[serde(default, rename_all = "camelCase")]
29pub struct ClientConfig {
30 #[serde(rename = "id", skip_serializing_if = "String::is_empty")]
32 pub protocol_id: String,
33 pub security: String,
35 #[serde(skip_serializing_if = "String::is_empty")]
37 pub password: String,
38 #[serde(skip_serializing_if = "String::is_empty")]
40 pub flow: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub reverse: Option<ClientReverse>,
44 #[serde(skip_serializing_if = "String::is_empty")]
46 pub auth: String,
47 #[serde(skip_serializing_if = "String::is_empty")]
49 pub private_key: String,
50 #[serde(skip_serializing_if = "String::is_empty")]
52 pub public_key: String,
53 #[serde(
55 rename = "allowedIPs",
56 skip_serializing_if = "Vec::is_empty",
57 deserialize_with = "deserialize_null_default"
58 )]
59 pub allowed_ips: Vec<String>,
60 #[serde(skip_serializing_if = "HashMap::is_empty")]
62 pub allowed_ips_by_inbound: HashMap<i64, Vec<String>>,
63 #[serde(skip_serializing_if = "String::is_empty")]
65 pub pre_shared_key: String,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub keep_alive: Option<i32>,
69 #[serde(skip_serializing_if = "String::is_empty")]
71 pub forwarded_ports: String,
72 #[serde(skip_serializing_if = "String::is_empty")]
74 pub secret: String,
75 #[serde(skip_serializing_if = "String::is_empty")]
77 pub ad_tag: String,
78 pub email: String,
80 pub limit_ip: i32,
82 pub limit_hwid: i32,
84 #[serde(rename = "totalGB")]
86 pub total_gb: i64,
87 pub expiry_time: i64,
89 pub enable: bool,
91 pub tg_id: i64,
93 pub sub_id: String,
95 #[serde(skip_serializing_if = "String::is_empty")]
97 pub group: String,
98 pub comment: String,
100 pub reset: i32,
102 pub reset_day: i32,
104 pub reset_max: i32,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub traffic_reset: Option<crate::TrafficReset>,
109 #[serde(skip_serializing_if = "is_zero_i32")]
111 pub traffic_reset_day: i32,
112 #[serde(rename = "created_at", skip_serializing_if = "is_zero_i64")]
114 pub created_at: i64,
115 #[serde(rename = "updated_at", skip_serializing_if = "is_zero_i64")]
117 pub updated_at: i64,
118}
119
120impl ClientConfig {
121 pub fn new(email: impl Into<String>) -> Self {
123 Self {
124 email: email.into(),
125 security: "auto".to_owned(),
126 enable: true,
127 ..Self::default()
128 }
129 }
130}
131
132impl fmt::Debug for ClientConfig {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 formatter
135 .debug_struct("ClientConfig")
136 .field("protocol_id", &"[REDACTED]")
137 .field("security", &self.security)
138 .field("password", &"[REDACTED]")
139 .field("flow", &self.flow)
140 .field("reverse", &self.reverse)
141 .field("auth", &"[REDACTED]")
142 .field("private_key", &"[REDACTED]")
143 .field("public_key", &"[REDACTED]")
144 .field("allowed_ips", &self.allowed_ips)
145 .field("allowed_ips_by_inbound", &self.allowed_ips_by_inbound)
146 .field("pre_shared_key", &"[REDACTED]")
147 .field("keep_alive", &self.keep_alive)
148 .field("forwarded_ports", &self.forwarded_ports)
149 .field("secret", &"[REDACTED]")
150 .field("ad_tag", &"[REDACTED]")
151 .field("email", &self.email)
152 .field("limit_ip", &self.limit_ip)
153 .field("limit_hwid", &self.limit_hwid)
154 .field("total_gb", &self.total_gb)
155 .field("expiry_time", &self.expiry_time)
156 .field("enable", &self.enable)
157 .field("tg_id", &self.tg_id)
158 .field("sub_id", &"[REDACTED]")
159 .field("group", &self.group)
160 .field("comment", &self.comment)
161 .field("reset", &self.reset)
162 .field("reset_day", &self.reset_day)
163 .field("reset_max", &self.reset_max)
164 .field("traffic_reset", &self.traffic_reset)
165 .field("traffic_reset_day", &self.traffic_reset_day)
166 .field("created_at", &self.created_at)
167 .field("updated_at", &self.updated_at)
168 .finish()
169 }
170}
171
172#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
174#[serde(default, rename_all = "camelCase")]
175pub struct ClientRecord {
176 pub id: i64,
178 pub email: String,
180 pub sub_id: String,
182 pub uuid: String,
184 pub password: String,
186 pub auth: String,
188 pub flow: String,
190 pub security: String,
192 pub reverse: Option<ClientReverse>,
194 pub private_key: String,
196 pub public_key: String,
198 #[serde(rename = "allowedIPs")]
200 pub allowed_ips: String,
201 pub pre_shared_key: String,
203 pub keep_alive: i32,
205 pub forwarded_ports: String,
207 pub secret: String,
209 pub ad_tag: String,
211 pub limit_ip: i32,
213 pub limit_hwid: i32,
215 #[serde(rename = "totalGB")]
217 pub total_gb: i64,
218 pub expiry_time: i64,
220 pub enable: bool,
222 pub tg_id: i64,
224 pub group: String,
226 pub comment: String,
228 pub reset: i32,
230 pub reset_day: i32,
232 pub reset_max: i32,
234 pub traffic_reset: String,
236 pub traffic_reset_day: i32,
238 pub created_at: i64,
240 pub updated_at: i64,
242}
243
244impl ClientRecord {
245 pub fn to_config(&self) -> ClientConfig {
247 ClientConfig {
248 protocol_id: self.uuid.clone(),
249 security: self.security.clone(),
250 password: self.password.clone(),
251 flow: self.flow.clone(),
252 reverse: self.reverse.clone(),
253 auth: self.auth.clone(),
254 private_key: self.private_key.clone(),
255 public_key: self.public_key.clone(),
256 allowed_ips: self
257 .allowed_ips
258 .split(',')
259 .map(str::trim)
260 .filter(|value| !value.is_empty())
261 .map(ToOwned::to_owned)
262 .collect(),
263 allowed_ips_by_inbound: HashMap::new(),
264 pre_shared_key: self.pre_shared_key.clone(),
265 keep_alive: Some(self.keep_alive),
266 forwarded_ports: self.forwarded_ports.clone(),
267 secret: self.secret.clone(),
268 ad_tag: self.ad_tag.clone(),
269 email: self.email.clone(),
270 limit_ip: self.limit_ip,
271 limit_hwid: self.limit_hwid,
272 total_gb: self.total_gb,
273 expiry_time: self.expiry_time,
274 enable: self.enable,
275 tg_id: self.tg_id,
276 sub_id: self.sub_id.clone(),
277 group: self.group.clone(),
278 comment: self.comment.clone(),
279 reset: self.reset,
280 reset_day: self.reset_day,
281 reset_max: self.reset_max,
282 traffic_reset: match self.traffic_reset.as_str() {
283 "hourly" => Some(crate::TrafficReset::Hourly),
284 "daily" => Some(crate::TrafficReset::Daily),
285 "weekly" => Some(crate::TrafficReset::Weekly),
286 "monthly" => Some(crate::TrafficReset::Monthly),
287 "never" => Some(crate::TrafficReset::Never),
288 _ => None,
289 },
290 traffic_reset_day: self.traffic_reset_day,
291 created_at: self.created_at,
292 updated_at: self.updated_at,
293 }
294 }
295}
296
297impl fmt::Debug for ClientRecord {
298 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
299 formatter
300 .debug_struct("ClientRecord")
301 .field("id", &self.id)
302 .field("email", &self.email)
303 .field("sub_id", &"[REDACTED]")
304 .field("uuid", &"[REDACTED]")
305 .field("password", &"[REDACTED]")
306 .field("auth", &"[REDACTED]")
307 .field("flow", &self.flow)
308 .field("security", &self.security)
309 .field("reverse", &self.reverse)
310 .field("private_key", &"[REDACTED]")
311 .field("public_key", &"[REDACTED]")
312 .field("allowed_ips", &self.allowed_ips)
313 .field("pre_shared_key", &"[REDACTED]")
314 .field("keep_alive", &self.keep_alive)
315 .field("forwarded_ports", &self.forwarded_ports)
316 .field("secret", &"[REDACTED]")
317 .field("ad_tag", &"[REDACTED]")
318 .field("limit_ip", &self.limit_ip)
319 .field("limit_hwid", &self.limit_hwid)
320 .field("total_gb", &self.total_gb)
321 .field("expiry_time", &self.expiry_time)
322 .field("enable", &self.enable)
323 .field("tg_id", &self.tg_id)
324 .field("group", &self.group)
325 .field("comment", &self.comment)
326 .field("reset", &self.reset)
327 .field("reset_day", &self.reset_day)
328 .field("reset_max", &self.reset_max)
329 .field("traffic_reset", &self.traffic_reset)
330 .field("traffic_reset_day", &self.traffic_reset_day)
331 .field("created_at", &self.created_at)
332 .field("updated_at", &self.updated_at)
333 .finish()
334 }
335}
336
337#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
339#[serde(default, rename_all = "camelCase")]
340pub struct ClientWithAttachments {
341 #[serde(flatten)]
343 pub client: ClientRecord,
344 #[serde(deserialize_with = "deserialize_null_default")]
346 pub inbound_ids: Vec<i64>,
347 pub traffic: Option<ClientTraffic>,
349}
350
351#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
353#[serde(rename_all = "camelCase")]
354pub struct ClientDetails {
355 pub client: ClientRecord,
357 #[serde(default, deserialize_with = "deserialize_null_default")]
359 pub inbound_ids: Vec<i64>,
360 #[serde(default, deserialize_with = "deserialize_null_default")]
362 pub external_links: Vec<ClientExternalLink>,
363 pub used_traffic: i64,
365 #[serde(default, deserialize_with = "deserialize_null_default")]
367 pub tunnel_allowed_ips: HashMap<i64, Vec<String>>,
368}
369
370#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
372#[serde(default, rename_all = "camelCase")]
373pub struct ClientHwidDevice {
374 pub id: i64,
376 pub first_seen: i64,
378 pub last_seen: i64,
380 pub user_agent: String,
382 #[serde(rename = "deviceOs")]
384 pub device_os: String,
385 pub os_version: String,
387 pub fingerprint: String,
389 pub device_model: String,
391}
392
393#[derive(Clone, Default, Deserialize, Eq, PartialEq)]
395#[serde(default, rename_all = "camelCase")]
396pub struct ClientSlim {
397 pub email: String,
399 pub sub_id: String,
401 pub enable: bool,
403 #[serde(rename = "totalGB")]
405 pub total_gb: i64,
406 pub expiry_time: i64,
408 pub limit_ip: i32,
410 pub reset: i32,
412 pub group: String,
414 pub comment: String,
416 #[serde(deserialize_with = "deserialize_null_default")]
418 pub inbound_ids: Vec<i64>,
419 pub traffic: Option<ClientTraffic>,
421 pub created_at: i64,
423 pub updated_at: i64,
425}
426
427impl fmt::Debug for ClientSlim {
428 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
429 formatter
430 .debug_struct("ClientSlim")
431 .field("email", &self.email)
432 .field("sub_id", &"[REDACTED]")
433 .field("enable", &self.enable)
434 .field("total_gb", &self.total_gb)
435 .field("expiry_time", &self.expiry_time)
436 .field("limit_ip", &self.limit_ip)
437 .field("reset", &self.reset)
438 .field("group", &self.group)
439 .field("comment", &self.comment)
440 .field("inbound_ids", &self.inbound_ids)
441 .field("traffic", &self.traffic)
442 .field("created_at", &self.created_at)
443 .field("updated_at", &self.updated_at)
444 .finish()
445 }
446}
447
448#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
450#[serde(default, rename_all = "camelCase")]
451pub struct ClientSummary {
452 pub total: u64,
454 pub active: u64,
456 pub online_count: u64,
458 pub depleted_count: u64,
460 pub expiring_count: u64,
462 pub deactive_count: u64,
464 #[serde(deserialize_with = "deserialize_null_default")]
466 pub online: Vec<String>,
467 #[serde(deserialize_with = "deserialize_null_default")]
469 pub depleted: Vec<String>,
470 #[serde(deserialize_with = "deserialize_null_default")]
472 pub expiring: Vec<String>,
473 #[serde(deserialize_with = "deserialize_null_default")]
475 pub deactive: Vec<String>,
476}
477
478#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
480#[serde(default, rename_all = "camelCase")]
481pub struct ClientPage {
482 #[serde(deserialize_with = "deserialize_null_default")]
484 pub items: Vec<ClientSlim>,
485 pub total: u64,
487 pub filtered: u64,
489 pub page: u32,
491 pub page_size: u16,
493 pub summary: ClientSummary,
495 #[serde(deserialize_with = "deserialize_null_default")]
497 pub groups: Vec<String>,
498}
499
500#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
502#[non_exhaustive]
503pub enum ClientStatusFilter {
504 Active,
506 Deactive,
508 Depleted,
510 Expiring,
512 Online,
514}
515
516impl ClientStatusFilter {
517 pub(crate) const fn as_str(self) -> &'static str {
518 match self {
519 Self::Active => "active",
520 Self::Deactive => "deactive",
521 Self::Depleted => "depleted",
522 Self::Expiring => "expiring",
523 Self::Online => "online",
524 }
525 }
526}
527
528#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
530#[non_exhaustive]
531pub enum ClientSort {
532 Enable,
534 Email,
536 InboundCount,
538 Traffic,
540 Remaining,
542 ExpiryTime,
544 CreatedAt,
546 UpdatedAt,
548 LastOnline,
550}
551
552impl ClientSort {
553 pub(crate) const fn as_str(self) -> &'static str {
554 match self {
555 Self::Enable => "enable",
556 Self::Email => "email",
557 Self::InboundCount => "inboundIds",
558 Self::Traffic => "traffic",
559 Self::Remaining => "remaining",
560 Self::ExpiryTime => "expiryTime",
561 Self::CreatedAt => "createdAt",
562 Self::UpdatedAt => "updatedAt",
563 Self::LastOnline => "lastOnline",
564 }
565 }
566}
567
568#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
570#[non_exhaustive]
571pub enum SortOrder {
572 #[default]
574 Ascending,
575 Descending,
577}
578
579impl SortOrder {
580 pub(crate) const fn as_str(self) -> &'static str {
581 match self {
582 Self::Ascending => "ascend",
583 Self::Descending => "descend",
584 }
585 }
586}
587
588#[derive(Clone, Debug, Eq, PartialEq)]
590pub struct ClientPageRequest {
591 pub page: u32,
593 pub page_size: u16,
595 pub search: String,
597 pub statuses: Vec<ClientStatusFilter>,
599 pub protocols: Vec<InboundProtocol>,
601 pub inbound_ids: Vec<i64>,
603 pub sort: Option<ClientSort>,
605 pub order: SortOrder,
607 pub expiry_from: Option<i64>,
609 pub expiry_to: Option<i64>,
611 pub usage_from: Option<i64>,
613 pub usage_to: Option<i64>,
615 pub auto_renew: Option<bool>,
617 pub has_telegram_id: Option<bool>,
619 pub has_comment: Option<bool>,
621 pub groups: Vec<String>,
623}
624
625impl Default for ClientPageRequest {
626 fn default() -> Self {
627 Self {
628 page: 1,
629 page_size: 25,
630 search: String::new(),
631 statuses: Vec::new(),
632 protocols: Vec::new(),
633 inbound_ids: Vec::new(),
634 sort: None,
635 order: SortOrder::Ascending,
636 expiry_from: None,
637 expiry_to: None,
638 usage_from: None,
639 usage_to: None,
640 auto_renew: None,
641 has_telegram_id: None,
642 has_comment: None,
643 groups: Vec::new(),
644 }
645 }
646}
647
648#[derive(Clone, Eq, PartialEq, Serialize)]
650#[serde(rename_all = "camelCase")]
651pub struct ClientCreateRequest {
652 pub client: ClientConfig,
654 pub inbound_ids: Vec<i64>,
656}
657
658impl ClientCreateRequest {
659 pub fn new(client: ClientConfig, inbound_ids: Vec<i64>) -> Self {
661 Self {
662 client,
663 inbound_ids,
664 }
665 }
666}
667
668impl fmt::Debug for ClientCreateRequest {
669 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
670 formatter
671 .debug_struct("ClientCreateRequest")
672 .field("client", &self.client)
673 .field("inbound_ids", &self.inbound_ids)
674 .finish()
675 }
676}
677
678impl<'de> Deserialize<'de> for ClientCreateRequest {
679 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
680 where
681 D: Deserializer<'de>,
682 {
683 #[derive(Deserialize)]
684 #[serde(rename_all = "camelCase")]
685 struct Wire {
686 client: ClientConfig,
687 #[serde(default, deserialize_with = "deserialize_null_default")]
688 inbound_ids: Vec<i64>,
689 }
690
691 Wire::deserialize(deserializer).map(|wire| Self {
692 client: wire.client,
693 inbound_ids: wire.inbound_ids,
694 })
695 }
696}
697
698#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
700#[serde(default, rename_all = "camelCase")]
701pub struct ClientMutationStatus {
702 pub node_pending: bool,
704}
705
706#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
708#[serde(rename_all = "lowercase")]
709#[non_exhaustive]
710pub enum ClientExternalLinkKind {
711 #[default]
713 Link,
714 Subscription,
716}
717
718#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
720#[serde(default, rename_all = "camelCase")]
721pub struct ClientExternalLinkInput {
722 pub kind: ClientExternalLinkKind,
724 pub value: String,
726 pub remark: String,
728}
729
730impl fmt::Debug for ClientExternalLinkInput {
731 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
732 formatter
733 .debug_struct("ClientExternalLinkInput")
734 .field("kind", &self.kind)
735 .field("value", &"[REDACTED]")
736 .field("remark", &self.remark)
737 .finish()
738 }
739}
740
741#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
743#[serde(default, rename_all = "camelCase")]
744pub struct ClientExternalLink {
745 pub id: i64,
747 pub client_id: i64,
749 pub kind: ClientExternalLinkKind,
751 pub value: String,
753 pub remark: String,
755 pub sort_index: i32,
757 pub created_at: i64,
759}
760
761impl fmt::Debug for ClientExternalLink {
762 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
763 formatter
764 .debug_struct("ClientExternalLink")
765 .field("id", &self.id)
766 .field("client_id", &self.client_id)
767 .field("kind", &self.kind)
768 .field("value", &"[REDACTED]")
769 .field("remark", &self.remark)
770 .field("sort_index", &self.sort_index)
771 .field("created_at", &self.created_at)
772 .finish()
773 }
774}
775
776#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
778#[non_exhaustive]
779pub enum BulkFlowAdjustment {
780 #[default]
782 Unchanged,
783 Clear,
785 Vision,
787 VisionUdp443,
789}
790
791impl BulkFlowAdjustment {
792 pub(crate) const fn as_str(self) -> &'static str {
793 match self {
794 Self::Unchanged => "",
795 Self::Clear => "none",
796 Self::Vision => "xtls-rprx-vision",
797 Self::VisionUdp443 => "xtls-rprx-vision-udp443",
798 }
799 }
800}
801
802#[derive(Clone, Default, Eq, PartialEq)]
804pub struct BulkAdjustRequest {
805 pub emails: Vec<String>,
808 pub add_days: i32,
810 pub add_bytes: i64,
812 pub flow: BulkFlowAdjustment,
814 pub limit_hwid: Option<i32>,
816 pub ad_tag: String,
818}
819
820impl fmt::Debug for BulkAdjustRequest {
821 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
823 formatter
824 .debug_struct("BulkAdjustRequest")
825 .field("emails", &self.emails)
826 .field("add_days", &self.add_days)
827 .field("add_bytes", &self.add_bytes)
828 .field("flow", &self.flow)
829 .field("limit_hwid", &self.limit_hwid)
830 .field("ad_tag", &"[REDACTED]")
831 .finish()
832 }
833}
834
835#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
837pub struct BulkClientIssue {
838 pub email: String,
840 pub reason: String,
842}
843
844#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
846pub struct BulkAdjustResult {
847 pub adjusted: u64,
849 #[serde(default, deserialize_with = "deserialize_null_default")]
852 pub skipped: Vec<BulkClientIssue>,
853}
854
855#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
857pub struct BulkCreateResult {
858 pub created: u64,
860 #[serde(default, deserialize_with = "deserialize_null_default")]
862 pub skipped: Vec<BulkClientIssue>,
863}
864
865#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
867pub struct BulkDeleteResult {
868 pub deleted: u64,
870 #[serde(default, deserialize_with = "deserialize_null_default")]
872 pub skipped: Vec<BulkClientIssue>,
873}
874
875#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
877pub struct BulkSetEnabledResult {
878 pub changed: u64,
880 #[serde(default, deserialize_with = "deserialize_null_default")]
882 pub skipped: Vec<BulkClientIssue>,
883}
884
885#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
887#[serde(default)]
888pub struct BulkAttachResult {
889 #[serde(deserialize_with = "deserialize_null_default")]
891 pub attached: Vec<String>,
892 #[serde(deserialize_with = "deserialize_null_default")]
894 pub skipped: Vec<String>,
895 #[serde(deserialize_with = "deserialize_null_default")]
897 pub errors: Vec<String>,
898}
899
900#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
902#[serde(default)]
903pub struct BulkDetachResult {
904 #[serde(deserialize_with = "deserialize_null_default")]
906 pub detached: Vec<String>,
907 #[serde(deserialize_with = "deserialize_null_default")]
909 pub skipped: Vec<String>,
910 #[serde(deserialize_with = "deserialize_null_default")]
912 pub errors: Vec<String>,
913}
914
915#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
917pub struct DeletedCount {
918 pub deleted: u64,
920}
921
922#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
924pub struct AffectedCount {
925 pub affected: u64,
927}
928
929#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
931pub struct GroupName {
932 pub name: String,
934}
935
936#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
938#[serde(default, rename_all = "camelCase")]
939pub struct GroupSummary {
940 pub name: String,
942 pub client_count: u64,
944 pub traffic_used: i64,
946 pub up: i64,
948 pub down: i64,
950}
951
952#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
954pub struct ClientIpEntry {
955 pub ip: String,
957 pub timestamp: i64,
959}
960
961#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
963pub struct ClientIpInfo {
964 pub ip: String,
966 pub time: String,
968 pub node: String,
970}
971
972pub type ClientsByGuid = HashMap<String, Vec<String>>;
974
975pub type ActiveInboundsByGuid = HashMap<String, Vec<String>>;
977
978pub type ClientIpsByGuid = HashMap<String, HashMap<String, Vec<ClientIpEntry>>>;
980
981pub type LastOnlineByEmail = HashMap<String, i64>;
983
984#[allow(clippy::trivially_copy_pass_by_ref)]
985const fn is_zero_i32(value: &i32) -> bool {
986 *value == 0
987}
988
989#[allow(clippy::trivially_copy_pass_by_ref)]
990const fn is_zero_i64(value: &i64) -> bool {
991 *value == 0
992}
993
994#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
996#[serde(rename_all = "camelCase")]
997pub struct HappLink {
998 pub encrypted_link: String,
1000}
1001
1002impl fmt::Debug for HappLink {
1003 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1005 formatter
1006 .debug_struct("HappLink")
1007 .field("encrypted_link", &"[REDACTED]")
1008 .finish()
1009 }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use super::*;
1015
1016 #[test]
1017 fn record_conversion_preserves_exact_acronym_wire_fields() {
1018 let record: ClientRecord = serde_json::from_value(serde_json::json!({
1019 "id": 9,
1020 "email": "alice",
1021 "uuid": "uuid-secret",
1022 "subId": "sub-secret",
1023 "allowedIPs": "10.0.0.2/32, fd00::2/128",
1024 "totalGB": 4096,
1025 "enable": true,
1026 "reverse": {"tag": "reverse-tag"}
1027 }))
1028 .unwrap();
1029 let config = record.to_config();
1030 let wire = serde_json::to_value(&config).unwrap();
1031
1032 assert_eq!(config.allowed_ips, ["10.0.0.2/32", "fd00::2/128"]);
1033 assert_eq!(
1034 wire["allowedIPs"],
1035 serde_json::json!(["10.0.0.2/32", "fd00::2/128"])
1036 );
1037 assert_eq!(wire["totalGB"], 4096);
1038 assert!(wire.get("allowedIps").is_none());
1039 assert!(wire.get("totalGb").is_none());
1040 }
1041
1042 #[test]
1043 fn client_debug_redacts_protocol_and_subscription_secrets() {
1044 let mut client = ClientConfig::new("alice");
1045 client.protocol_id = "uuid-secret".to_owned();
1046 client.password = "password-secret".to_owned();
1047 client.auth = "auth-secret".to_owned();
1048 client.private_key = "private-secret".to_owned();
1049 client.pre_shared_key = "psk-secret".to_owned();
1050 client.secret = "mtproto-secret".to_owned();
1051 client.sub_id = "subscription-secret".to_owned();
1052 let output = format!("{client:?}");
1053
1054 for secret in [
1055 "uuid-secret",
1056 "password-secret",
1057 "auth-secret",
1058 "private-secret",
1059 "psk-secret",
1060 "mtproto-secret",
1061 "subscription-secret",
1062 ] {
1063 assert!(!output.contains(secret));
1064 }
1065 assert!(output.contains("alice"));
1066
1067 let slim = ClientSlim {
1068 email: "alice".to_owned(),
1069 sub_id: "slim-subscription-secret".to_owned(),
1070 ..ClientSlim::default()
1071 };
1072 assert!(!format!("{slim:?}").contains("slim-subscription-secret"));
1073 }
1074
1075 #[test]
1076 fn nil_go_slices_decode_as_empty_rust_vectors() {
1077 let result: BulkAttachResult = serde_json::from_value(serde_json::json!({
1078 "attached": null,
1079 "skipped": null,
1080 "errors": null
1081 }))
1082 .unwrap();
1083 let client: ClientWithAttachments = serde_json::from_value(serde_json::json!({
1084 "email": "orphan",
1085 "inboundIds": null
1086 }))
1087 .unwrap();
1088
1089 assert!(result.attached.is_empty());
1090 assert!(result.skipped.is_empty());
1091 assert!(result.errors.is_empty());
1092 assert!(client.inbound_ids.is_empty());
1093 }
1094
1095 #[test]
1096 fn record_and_link_debug_views_redact_credentials() {
1097 let record = ClientRecord {
1098 email: "alice".into(),
1099 uuid: "record-uuid-secret".into(),
1100 password: "record-password-secret".into(),
1101 auth: "record-auth-secret".into(),
1102 private_key: "record-private-secret".into(),
1103 public_key: "record-public-secret".into(),
1104 pre_shared_key: "record-psk-secret".into(),
1105 secret: "record-mtproto-secret".into(),
1106 ad_tag: "record-ad-secret".into(),
1107 sub_id: "record-sub-secret".into(),
1108 ..ClientRecord::default()
1109 };
1110 let record_debug = format!("{record:?}");
1111 assert!(record_debug.contains("alice"));
1112 for secret in [
1113 "record-uuid-secret",
1114 "record-password-secret",
1115 "record-auth-secret",
1116 "record-private-secret",
1117 "record-public-secret",
1118 "record-psk-secret",
1119 "record-mtproto-secret",
1120 "record-ad-secret",
1121 "record-sub-secret",
1122 ] {
1123 assert!(!record_debug.contains(secret));
1124 }
1125
1126 let request = ClientCreateRequest::new(ClientConfig::new("bob"), vec![7, 9]);
1127 assert!(format!("{request:?}").contains("bob"));
1128 let input = ClientExternalLinkInput {
1129 kind: ClientExternalLinkKind::Subscription,
1130 value: "https://example.com/input-link-secret".into(),
1131 remark: "primary".into(),
1132 };
1133 let link = ClientExternalLink {
1134 id: 3,
1135 client_id: 4,
1136 kind: ClientExternalLinkKind::Link,
1137 value: "vless://persisted-link-secret".into(),
1138 remark: "backup".into(),
1139 sort_index: 2,
1140 created_at: 5,
1141 };
1142 assert!(!format!("{input:?}").contains("input-link-secret"));
1143 assert!(!format!("{link:?}").contains("persisted-link-secret"));
1144 }
1145
1146 #[test]
1147 fn paging_and_bulk_enums_match_source_vocabulary() {
1148 assert_eq!(
1149 [
1150 ClientStatusFilter::Active,
1151 ClientStatusFilter::Deactive,
1152 ClientStatusFilter::Depleted,
1153 ClientStatusFilter::Expiring,
1154 ClientStatusFilter::Online,
1155 ]
1156 .map(ClientStatusFilter::as_str),
1157 ["active", "deactive", "depleted", "expiring", "online"]
1158 );
1159 assert_eq!(
1160 [
1161 ClientSort::Enable,
1162 ClientSort::Email,
1163 ClientSort::InboundCount,
1164 ClientSort::Traffic,
1165 ClientSort::Remaining,
1166 ClientSort::ExpiryTime,
1167 ClientSort::CreatedAt,
1168 ClientSort::UpdatedAt,
1169 ClientSort::LastOnline,
1170 ]
1171 .map(ClientSort::as_str),
1172 [
1173 "enable",
1174 "email",
1175 "inboundIds",
1176 "traffic",
1177 "remaining",
1178 "expiryTime",
1179 "createdAt",
1180 "updatedAt",
1181 "lastOnline",
1182 ]
1183 );
1184 assert_eq!(SortOrder::Ascending.as_str(), "ascend");
1185 assert_eq!(SortOrder::Descending.as_str(), "descend");
1186 assert_eq!(
1187 [
1188 BulkFlowAdjustment::Unchanged,
1189 BulkFlowAdjustment::Clear,
1190 BulkFlowAdjustment::Vision,
1191 BulkFlowAdjustment::VisionUdp443,
1192 ]
1193 .map(BulkFlowAdjustment::as_str),
1194 ["", "none", "xtls-rprx-vision", "xtls-rprx-vision-udp443"]
1195 );
1196 }
1197
1198 #[test]
1199 fn create_request_accepts_legacy_null_inbound_ids() {
1200 let request: ClientCreateRequest = serde_json::from_value(serde_json::json!({
1201 "client": {"email": "alice"},
1202 "inboundIds": null
1203 }))
1204 .unwrap();
1205 assert_eq!(request.client.email, "alice");
1206 assert!(request.inbound_ids.is_empty());
1207 }
1208}