1use secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Serialize};
9
10pub const SCHEMA_VERSION_ATUAL: u32 = 1;
12
13pub const TIMEOUT_PADRAO_MS: u64 = 30_000;
15
16pub const MAX_CHARS_PADRAO: usize = 100_000;
18
19#[derive(Clone, Serialize, Deserialize)]
21pub struct VpsRegistro {
22 pub nome: String,
24 pub host: String,
26 pub porta: u16,
28 pub usuario: String,
30 #[serde(with = "secret_string_serde")]
32 pub senha: SecretString,
33 pub timeout_ms: u64,
35 pub max_chars: usize,
37 #[serde(default, with = "opcao_secret_string_serde")]
39 pub senha_sudo: Option<SecretString>,
40 #[serde(default, with = "opcao_secret_string_serde")]
42 pub senha_su: Option<SecretString>,
43 pub schema_version: u32,
45 pub adicionado_em: String,
47}
48
49impl std::fmt::Debug for VpsRegistro {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("VpsRegistro")
52 .field("nome", &self.nome)
53 .field("host", &self.host)
54 .field("porta", &self.porta)
55 .field("usuario", &self.usuario)
56 .field("senha", &"<redacted>")
57 .field("timeout_ms", &self.timeout_ms)
58 .field("max_chars", &self.max_chars)
59 .field(
60 "senha_sudo",
61 &self.senha_sudo.as_ref().map(|_| "<redacted>"),
62 )
63 .field("senha_su", &self.senha_su.as_ref().map(|_| "<redacted>"))
64 .field("schema_version", &self.schema_version)
65 .field("adicionado_em", &self.adicionado_em)
66 .finish()
67 }
68}
69
70impl VpsRegistro {
71 #[must_use]
73 #[allow(clippy::too_many_arguments)]
74 pub fn novo(
75 nome: String,
76 host: String,
77 porta: u16,
78 usuario: String,
79 senha: SecretString,
80 timeout_ms: Option<u64>,
81 max_chars: Option<usize>,
82 senha_sudo: Option<SecretString>,
83 senha_su: Option<SecretString>,
84 ) -> Self {
85 Self {
86 nome,
87 host,
88 porta,
89 usuario,
90 senha,
91 timeout_ms: timeout_ms.unwrap_or(TIMEOUT_PADRAO_MS),
92 max_chars: max_chars.unwrap_or(MAX_CHARS_PADRAO),
93 senha_sudo,
94 senha_su,
95 schema_version: SCHEMA_VERSION_ATUAL,
96 adicionado_em: chrono::Utc::now().to_rfc3339(),
97 }
98 }
99}
100
101mod secret_string_serde {
102 use super::{ExposeSecret, SecretString};
103 use serde::{Deserialize, Deserializer, Serializer};
104
105 pub fn serialize<S: Serializer>(valor: &SecretString, s: S) -> Result<S::Ok, S::Error> {
106 s.serialize_str(valor.expose_secret())
107 }
108
109 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SecretString, D::Error> {
110 let s = String::deserialize(d)?;
111 Ok(SecretString::from(s))
112 }
113}
114
115mod opcao_secret_string_serde {
116 use super::{ExposeSecret, SecretString};
117 use serde::{Deserialize, Deserializer, Serializer};
118
119 pub fn serialize<S: Serializer>(valor: &Option<SecretString>, s: S) -> Result<S::Ok, S::Error> {
120 match valor {
121 Some(v) => s.serialize_some(v.expose_secret()),
122 None => s.serialize_none(),
123 }
124 }
125
126 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<SecretString>, D::Error> {
127 let opt = Option::<String>::deserialize(d)?;
128 Ok(opt.map(SecretString::from))
129 }
130}
131
132#[cfg(test)]
133mod testes {
134 use super::*;
135
136 #[test]
137 fn novo_registro_aplica_defaults() {
138 let r = VpsRegistro::novo(
139 "teste".into(),
140 "1.2.3.4".into(),
141 22,
142 "root".into(),
143 SecretString::from("senha".to_string()),
144 None,
145 None,
146 None,
147 None,
148 );
149 assert_eq!(r.timeout_ms, TIMEOUT_PADRAO_MS);
150 assert_eq!(r.max_chars, MAX_CHARS_PADRAO);
151 assert_eq!(r.schema_version, SCHEMA_VERSION_ATUAL);
152 assert!(!r.adicionado_em.is_empty());
153 }
154
155 #[test]
156 fn debug_nao_exibe_senha() {
157 let r = VpsRegistro::novo(
158 "t".into(),
159 "h".into(),
160 22,
161 "u".into(),
162 SecretString::from("senha-super-secreta".to_string()),
163 None,
164 None,
165 None,
166 None,
167 );
168 let dbg = format!("{r:?}");
169 assert!(!dbg.contains("senha-super-secreta"));
170 assert!(dbg.contains("redacted"));
171 }
172
173 #[test]
174 fn round_trip_toml_preserva_dados() {
175 let r = VpsRegistro::novo(
176 "producao".into(),
177 "srv.exemplo.com".into(),
178 2222,
179 "admin".into(),
180 SecretString::from("senha-do-admin-longa".to_string()),
181 Some(5000),
182 Some(50_000),
183 Some(SecretString::from("sudopass".to_string())),
184 None,
185 );
186 let toml_str = toml::to_string(&r).expect("serializar");
187 let r2: VpsRegistro = toml::from_str(&toml_str).expect("deserializar");
188 assert_eq!(r2.nome, "producao");
189 assert_eq!(r2.porta, 2222);
190 assert_eq!(r2.senha.expose_secret(), "senha-do-admin-longa");
191 assert_eq!(
192 r2.senha_sudo
193 .as_ref()
194 .map(|s| s.expose_secret().to_string()),
195 Some("sudopass".to_string())
196 );
197 assert!(r2.senha_su.is_none());
198 }
199}