@@ -4,22 +4,28 @@ use schemars::JsonSchema;
44use serde:: { Deserialize , Serialize } ;
55
66use crate :: application:: command_handlers:: create:: config:: errors:: CreateConfigError ;
7- use crate :: application:: command_handlers:: create:: config:: https:: TlsSection ;
8- use crate :: domain:: tls:: TlsConfig ;
97use crate :: domain:: tracker:: HealthCheckApiConfig ;
108use crate :: shared:: DomainName ;
119
1210#[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , JsonSchema ) ]
1311pub struct HealthCheckApiSection {
1412 pub bind_address : String ,
1513
16- /// Optional TLS configuration for HTTPS
14+ /// Domain name for HTTPS access via Caddy reverse proxy
1715 ///
18- /// When present, this service will be proxied through Caddy with HTTPS enabled.
19- /// The domain specified will be used for Let's Encrypt certificate acquisition.
16+ /// When present with `use_tls_proxy: true`, this service will be accessible
17+ /// via HTTPS at this domain. The domain will be used for Let's Encrypt
18+ /// certificate acquisition.
19+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
20+ pub domain : Option < String > ,
21+
22+ /// Whether to proxy this service through Caddy with TLS termination
23+ ///
24+ /// When `true`, the service will be accessible via HTTPS through Caddy.
25+ /// Requires `domain` to be set.
2026 /// This is useful for exposing health checks to external monitoring systems.
2127 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
22- pub tls : Option < TlsSection > ,
28+ pub use_tls_proxy : Option < bool > ,
2329}
2430
2531impl HealthCheckApiSection {
@@ -29,7 +35,8 @@ impl HealthCheckApiSection {
2935 ///
3036 /// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
3137 /// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
32- /// Returns `CreateConfigError::InvalidDomain` if the TLS domain is invalid.
38+ /// Returns `CreateConfigError::InvalidDomain` if the domain is invalid.
39+ /// Returns `CreateConfigError::TlsProxyWithoutDomain` if `use_tls_proxy` is true but domain is missing.
3340 ///
3441 /// Note: Localhost + TLS validation is performed at the domain layer
3542 /// (see `TrackerConfig::validate()`) to avoid duplicating business rules.
@@ -49,30 +56,42 @@ impl HealthCheckApiSection {
4956 } ) ;
5057 }
5158
52- // Convert TLS section to domain type with validation
53- let tls = match & self . tls {
54- Some ( tls_section) => {
55- tls_section. validate ( ) ?;
56- let domain = DomainName :: new ( & tls_section. domain ) . map_err ( |e| {
59+ let use_tls_proxy = self . use_tls_proxy . unwrap_or ( false ) ;
60+
61+ // Validate: use_tls_proxy requires domain
62+ if use_tls_proxy && self . domain . is_none ( ) {
63+ return Err ( CreateConfigError :: TlsProxyWithoutDomain {
64+ service_type : "Health Check API" . to_string ( ) ,
65+ bind_address : self . bind_address . clone ( ) ,
66+ } ) ;
67+ }
68+
69+ // Parse domain if present
70+ let domain =
71+ match & self . domain {
72+ Some ( domain_str) => Some ( DomainName :: new ( domain_str) . map_err ( |e| {
5773 CreateConfigError :: InvalidDomain {
58- domain : tls_section . domain . clone ( ) ,
74+ domain : domain_str . clone ( ) ,
5975 reason : e. to_string ( ) ,
6076 }
61- } ) ?;
62- Some ( TlsConfig :: new ( domain) )
63- }
64- None => None ,
65- } ;
66-
67- Ok ( HealthCheckApiConfig { bind_address, tls } )
77+ } ) ?) ,
78+ None => None ,
79+ } ;
80+
81+ Ok ( HealthCheckApiConfig {
82+ bind_address,
83+ domain,
84+ use_tls_proxy,
85+ } )
6886 }
6987}
7088
7189impl Default for HealthCheckApiSection {
7290 fn default ( ) -> Self {
7391 Self {
7492 bind_address : "127.0.0.1:1313" . to_string ( ) ,
75- tls : None ,
93+ domain : None ,
94+ use_tls_proxy : None ,
7695 }
7796 }
7897}
@@ -85,7 +104,8 @@ mod tests {
85104 fn it_should_convert_to_domain_config_when_bind_address_is_valid ( ) {
86105 let section = HealthCheckApiSection {
87106 bind_address : "127.0.0.1:1313" . to_string ( ) ,
88- tls : None ,
107+ domain : None ,
108+ use_tls_proxy : None ,
89109 } ;
90110
91111 let config = section. to_health_check_api_config ( ) . unwrap ( ) ;
@@ -94,16 +114,16 @@ mod tests {
94114 config. bind_address,
95115 "127.0.0.1:1313" . parse:: <SocketAddr >( ) . unwrap( )
96116 ) ;
97- assert ! ( config. tls. is_none( ) ) ;
117+ assert ! ( !config. use_tls_proxy) ;
118+ assert ! ( config. domain. is_none( ) ) ;
98119 }
99120
100121 #[ test]
101- fn it_should_convert_to_domain_config_with_tls ( ) {
122+ fn it_should_convert_to_domain_config_with_tls_proxy ( ) {
102123 let section = HealthCheckApiSection {
103124 bind_address : "0.0.0.0:1313" . to_string ( ) ,
104- tls : Some ( TlsSection {
105- domain : "health.tracker.local" . to_string ( ) ,
106- } ) ,
125+ domain : Some ( "health.tracker.local" . to_string ( ) ) ,
126+ use_tls_proxy : Some ( true ) ,
107127 } ;
108128
109129 let config = section. to_health_check_api_config ( ) . unwrap ( ) ;
@@ -112,15 +132,16 @@ mod tests {
112132 config. bind_address,
113133 "0.0.0.0:1313" . parse:: <SocketAddr >( ) . unwrap( )
114134 ) ;
115- assert ! ( config. tls . is_some ( ) ) ;
135+ assert ! ( config. use_tls_proxy ) ;
116136 assert_eq ! ( config. tls_domain( ) , Some ( "health.tracker.local" ) ) ;
117137 }
118138
119139 #[ test]
120140 fn it_should_fail_when_bind_address_is_invalid ( ) {
121141 let section = HealthCheckApiSection {
122142 bind_address : "invalid" . to_string ( ) ,
123- tls : None ,
143+ domain : None ,
144+ use_tls_proxy : None ,
124145 } ;
125146
126147 let result = section. to_health_check_api_config ( ) ;
@@ -136,7 +157,8 @@ mod tests {
136157 fn it_should_reject_dynamic_port_assignment ( ) {
137158 let section = HealthCheckApiSection {
138159 bind_address : "0.0.0.0:0" . to_string ( ) ,
139- tls : None ,
160+ domain : None ,
161+ use_tls_proxy : None ,
140162 } ;
141163
142164 let result = section. to_health_check_api_config ( ) ;
@@ -152,7 +174,8 @@ mod tests {
152174 fn it_should_allow_ipv6_addresses ( ) {
153175 let section = HealthCheckApiSection {
154176 bind_address : "[::1]:1313" . to_string ( ) ,
155- tls : None ,
177+ domain : None ,
178+ use_tls_proxy : None ,
156179 } ;
157180
158181 let result = section. to_health_check_api_config ( ) ;
@@ -164,7 +187,8 @@ mod tests {
164187 fn it_should_allow_any_port_except_zero ( ) {
165188 let section = HealthCheckApiSection {
166189 bind_address : "127.0.0.1:8080" . to_string ( ) ,
167- tls : None ,
190+ domain : None ,
191+ use_tls_proxy : None ,
168192 } ;
169193
170194 let result = section. to_health_check_api_config ( ) ;
@@ -177,16 +201,16 @@ mod tests {
177201 let section = HealthCheckApiSection :: default ( ) ;
178202
179203 assert_eq ! ( section. bind_address, "127.0.0.1:1313" ) ;
180- assert ! ( section. tls. is_none( ) ) ;
204+ assert ! ( section. domain. is_none( ) ) ;
205+ assert ! ( section. use_tls_proxy. is_none( ) ) ;
181206 }
182207
183208 #[ test]
184- fn it_should_fail_when_tls_domain_is_invalid ( ) {
209+ fn it_should_fail_when_domain_is_invalid ( ) {
185210 let section = HealthCheckApiSection {
186211 bind_address : "0.0.0.0:1313" . to_string ( ) ,
187- tls : Some ( TlsSection {
188- domain : "invalid domain with spaces" . to_string ( ) ,
189- } ) ,
212+ domain : Some ( "invalid domain with spaces" . to_string ( ) ) ,
213+ use_tls_proxy : Some ( true ) ,
190214 } ;
191215
192216 let result = section. to_health_check_api_config ( ) ;
@@ -197,4 +221,35 @@ mod tests {
197221 CreateConfigError :: InvalidDomain { .. }
198222 ) ) ;
199223 }
224+
225+ #[ test]
226+ fn it_should_fail_when_use_tls_proxy_without_domain ( ) {
227+ let section = HealthCheckApiSection {
228+ bind_address : "0.0.0.0:1313" . to_string ( ) ,
229+ domain : None ,
230+ use_tls_proxy : Some ( true ) ,
231+ } ;
232+
233+ let result = section. to_health_check_api_config ( ) ;
234+
235+ assert ! ( result. is_err( ) ) ;
236+ assert ! ( matches!(
237+ result. unwrap_err( ) ,
238+ CreateConfigError :: TlsProxyWithoutDomain { .. }
239+ ) ) ;
240+ }
241+
242+ #[ test]
243+ fn it_should_allow_domain_without_tls_proxy ( ) {
244+ let section = HealthCheckApiSection {
245+ bind_address : "0.0.0.0:1313" . to_string ( ) ,
246+ domain : Some ( "health.tracker.local" . to_string ( ) ) ,
247+ use_tls_proxy : None ,
248+ } ;
249+
250+ let config = section. to_health_check_api_config ( ) . unwrap ( ) ;
251+
252+ assert ! ( !config. use_tls_proxy) ;
253+ assert ! ( config. domain. is_some( ) ) ;
254+ }
200255}
0 commit comments