Skip to content

Commit 5334429

Browse files
committed
feat: [#272] Handle localhost-bound services in show command and validation (Task 7.4)
- Add LocalhostWithTls error to reject localhost + TLS combinations in domain layer - Add is_localhost helper function for detecting 127.0.0.1 and ::1 - Add is_localhost_only fields to ServiceInfo for API, health check, HTTP trackers - Update show command to display "Internal only" for localhost services - Add SSH tunnel hint for localhost-only services - Validation logic in TrackerConfig::validate() to avoid duplication
1 parent d21f313 commit 5334429

File tree

12 files changed

+689
-27
lines changed

12 files changed

+689
-27
lines changed

docs/issues/272-add-https-support-with-caddy.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,7 @@ Internal ports (1212, 7070, 7071, 3000) are not directly accessible when TLS is
10811081

10821082
> **Note**: JSON schema regeneration deferred to Phase 8.
10831083

1084-
#### 7.4: Handle Localhost-Bound Services in Show Command and Validation
1084+
#### 7.4: Handle Localhost-Bound Services in Show Command and Validation ✅ COMPLETE
10851085

10861086
**Current State**:
10871087

@@ -1145,11 +1145,11 @@ Health Check:
11451145

11461146
**Implementation Scope**:
11471147

1148-
- [ ] Add validation in domain layer to reject localhost + TLS combinations (during DTO-to-domain conversion)
1149-
- [ ] Update show command to detect localhost-bound services
1150-
- [ ] Add `is_localhost_only` field to `ServiceInfo` for health check, API, and HTTP trackers
1151-
- [ ] Display "Internal only" message for internal-only services
1152-
- [ ] Apply to: health check API, HTTP API, HTTP trackers (Grafana excluded - hardcoded port)
1148+
- [x] Add validation in domain layer to reject localhost + TLS combinations (during DTO-to-domain conversion)
1149+
- [x] Update show command to detect localhost-bound services
1150+
- [x] Add `is_localhost_only` field to `ServiceInfo` for health check, API, and HTTP trackers
1151+
- [x] Display "Internal only" message for internal-only services
1152+
- [x] Apply to: health check API, HTTP API, HTTP trackers (Grafana excluded - hardcoded port)
11531153

11541154
### Phase 8: Schema Generation (30 minutes)
11551155

src/application/command_handlers/create/config/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ mod tests {
675675
let help = error.help();
676676
assert!(!help.is_empty(), "Help text should not be empty");
677677
assert!(
678-
help.contains("Fix:") || help.contains("Common"),
678+
help.contains("Fix") || help.contains("Common"),
679679
"Help should contain actionable guidance"
680680
);
681681
}

src/application/command_handlers/create/config/tracker/health_check_api_section.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ impl HealthCheckApiSection {
3030
/// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
3131
/// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
3232
/// Returns `CreateConfigError::InvalidDomain` if the TLS domain is invalid.
33+
///
34+
/// Note: Localhost + TLS validation is performed at the domain layer
35+
/// (see `TrackerConfig::validate()`) to avoid duplicating business rules.
3336
pub fn to_health_check_api_config(&self) -> Result<HealthCheckApiConfig, CreateConfigError> {
3437
// Validate that the bind address can be parsed as SocketAddr
3538
let bind_address = self.bind_address.parse::<SocketAddr>().map_err(|e| {

src/application/command_handlers/create/config/tracker/http_api_section.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ impl HttpApiSection {
3131
/// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
3232
/// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
3333
/// Returns `CreateConfigError::InvalidDomain` if the TLS domain is invalid.
34+
///
35+
/// Note: Localhost + TLS validation is performed at the domain layer
36+
/// (see `TrackerConfig::validate()`) to avoid duplicating business rules.
3437
pub fn to_http_api_config(&self) -> Result<HttpApiConfig, CreateConfigError> {
3538
// Validate that the bind address can be parsed as SocketAddr
3639
let bind_address = self.bind_address.parse::<SocketAddr>().map_err(|e| {
@@ -151,4 +154,21 @@ mod tests {
151154
assert_eq!(section.bind_address, "0.0.0.0:1212");
152155
assert_eq!(section.admin_token, "MyAccessToken");
153156
}
157+
158+
#[test]
159+
fn it_should_allow_non_localhost_with_tls() {
160+
let section = HttpApiSection {
161+
bind_address: "0.0.0.0:1212".to_string(),
162+
admin_token: "token".to_string(),
163+
tls: Some(TlsSection {
164+
domain: "api.tracker.local".to_string(),
165+
}),
166+
};
167+
168+
let result = section.to_http_api_config();
169+
170+
assert!(result.is_ok());
171+
let config = result.unwrap();
172+
assert!(config.tls.is_some());
173+
}
154174
}

src/application/command_handlers/create/config/tracker/http_tracker_section.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ impl HttpTrackerSection {
2929
/// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
3030
/// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
3131
/// Returns `CreateConfigError::InvalidDomain` if the TLS domain is invalid.
32+
///
33+
/// Note: Localhost + TLS validation is performed at the domain layer
34+
/// (see `TrackerConfig::validate()`) to avoid duplicating business rules.
3235
pub fn to_http_tracker_config(&self) -> Result<HttpTrackerConfig, CreateConfigError> {
3336
// Validate that the bind address can be parsed as SocketAddr
3437
let bind_address = self.bind_address.parse::<SocketAddr>().map_err(|e| {
@@ -137,4 +140,20 @@ mod tests {
137140
let section: HttpTrackerSection = serde_json::from_str(json).unwrap();
138141
assert_eq!(section.bind_address, "0.0.0.0:7070");
139142
}
143+
144+
#[test]
145+
fn it_should_allow_non_localhost_with_tls() {
146+
let section = HttpTrackerSection {
147+
bind_address: "0.0.0.0:7070".to_string(),
148+
tls: Some(TlsSection {
149+
domain: "tracker.local".to_string(),
150+
}),
151+
};
152+
153+
let result = section.to_http_tracker_config();
154+
155+
assert!(result.is_ok());
156+
let config = result.unwrap();
157+
assert!(config.tls.is_some());
158+
}
140159
}

src/application/command_handlers/show/info/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use chrono::{DateTime, Utc};
2121

2222
pub use self::grafana::GrafanaInfo;
2323
pub use self::prometheus::PrometheusInfo;
24-
pub use self::tracker::{ServiceInfo, TlsDomainInfo};
24+
pub use self::tracker::{LocalhostServiceInfo, ServiceInfo, TlsDomainInfo};
2525

2626
/// Environment information for display purposes
2727
///

0 commit comments

Comments
 (0)