Skip to main content

systemprompt_models/profile/
validation.rs

1//! Profile validation logic.
2//!
3//! This module contains all validation logic for Profile configurations,
4//! including path validation, security settings, CORS, and rate limits.
5
6use std::path::Path;
7
8use super::{Profile, ProfileError, ProfileResult};
9
10impl Profile {
11    pub fn validate(&self) -> ProfileResult<()> {
12        let mut errors: Vec<String> = Vec::new();
13        let is_cloud = self.target.is_cloud();
14
15        self.validate_required_fields(&mut errors);
16        self.validate_paths(&mut errors, is_cloud);
17        self.validate_security_settings(&mut errors);
18        self.validate_cors_origins(&mut errors);
19        self.validate_rate_limits(&mut errors);
20
21        if errors.is_empty() {
22            Ok(())
23        } else {
24            Err(ProfileError::Validation {
25                name: self.name.clone(),
26                errors,
27            })
28        }
29    }
30
31    pub(super) fn validate_paths(&self, errors: &mut Vec<String>, is_cloud: bool) {
32        if is_cloud {
33            self.validate_cloud_paths(errors);
34        } else {
35            self.validate_local_paths(errors);
36        }
37    }
38
39    pub(super) fn validate_cloud_paths(&self, errors: &mut Vec<String>) {
40        Self::require_non_empty(errors, &self.paths.system, "Paths system");
41        Self::require_non_empty(errors, &self.paths.services, "Paths services");
42        Self::require_non_empty(errors, &self.paths.bin, "Paths bin");
43
44        for (name, path) in [
45            ("system", self.paths.system.as_str()),
46            ("services", self.paths.services.as_str()),
47            ("bin", self.paths.bin.as_str()),
48        ] {
49            if !path.is_empty() && !path.starts_with("/app") {
50                errors.push(format!(
51                    "Cloud profile {} path should start with /app, got: {}",
52                    name, path
53                ));
54            }
55        }
56
57        if let Some(web_path) = &self.paths.web_path {
58            if !web_path.is_empty() {
59                if !web_path.starts_with("/app/web") {
60                    errors.push(format!(
61                        "Cloud profile web_path should start with /app/web, got: {}. Note: \
62                         web_path points to the parent of dist/, e.g., /app/web for /app/web/dist",
63                        web_path
64                    ));
65                }
66                if web_path.contains("/services/web") {
67                    errors.push(format!(
68                        "Cloud profile web_path should be /app/web (for dist output), not \
69                         /app/services/web (which is for templates/config). Got: {}",
70                        web_path
71                    ));
72                }
73            }
74        }
75    }
76
77    pub(super) fn validate_local_paths(&self, errors: &mut Vec<String>) {
78        Self::validate_local_required_path(errors, "system", &self.paths.system);
79        Self::validate_local_required_path(errors, "services", &self.paths.services);
80        Self::validate_local_required_path(errors, "bin", &self.paths.bin);
81
82        Self::validate_local_optional_path(errors, "storage", self.paths.storage.as_ref());
83        Self::validate_local_optional_path(
84            errors,
85            "geoip_database",
86            self.paths.geoip_database.as_ref(),
87        );
88        Self::validate_local_optional_path(errors, "web_path", self.paths.web_path.as_ref());
89    }
90
91    fn validate_local_required_path(errors: &mut Vec<String>, name: &str, path: &str) {
92        if path.is_empty() {
93            errors.push(format!("Paths {} is required", name));
94            return;
95        }
96
97        if !Path::new(path).exists() {
98            errors.push(format!("{} path does not exist: {}", name, path));
99        }
100    }
101
102    fn validate_local_optional_path(errors: &mut Vec<String>, name: &str, path: Option<&String>) {
103        if let Some(p) = path {
104            if !p.is_empty() && !Path::new(p).exists() {
105                errors.push(format!("paths.{} does not exist: {}", name, p));
106            }
107        }
108    }
109
110    pub(super) fn validate_required_fields(&self, errors: &mut Vec<String>) {
111        Self::require_non_empty(errors, &self.name, "Profile name");
112        Self::require_non_empty(errors, &self.display_name, "Profile display_name");
113        Self::require_non_empty(errors, &self.site.name, "Site name");
114        Self::require_non_empty(errors, &self.server.host, "Server host");
115        Self::require_non_empty(errors, &self.server.api_server_url, "Server api_server_url");
116        Self::require_non_empty(
117            errors,
118            &self.server.api_internal_url,
119            "Server api_internal_url",
120        );
121        Self::require_non_empty(
122            errors,
123            &self.server.api_external_url,
124            "Server api_external_url",
125        );
126
127        if self.server.port == 0 {
128            errors.push("Server port must be greater than 0".to_string());
129        }
130    }
131
132    pub(super) fn require_non_empty(errors: &mut Vec<String>, value: &str, field_name: &str) {
133        if value.is_empty() {
134            errors.push(format!("{field_name} is required"));
135        }
136    }
137
138    pub(super) fn validate_security_settings(&self, errors: &mut Vec<String>) {
139        if self.security.access_token_expiration <= 0 {
140            errors.push("Security access_token_expiration must be positive".to_string());
141        }
142
143        if self.security.refresh_token_expiration <= 0 {
144            errors.push("Security refresh_token_expiration must be positive".to_string());
145        }
146    }
147
148    pub(super) fn validate_cors_origins(&self, errors: &mut Vec<String>) {
149        for origin in &self.server.cors_allowed_origins {
150            if origin.is_empty() {
151                errors.push("CORS origin cannot be empty".to_string());
152                continue;
153            }
154
155            let is_valid = origin.starts_with("http://") || origin.starts_with("https://");
156            if !is_valid {
157                errors.push(format!(
158                    "Invalid CORS origin (must start with http:// or https://): {}",
159                    origin
160                ));
161            }
162        }
163    }
164
165    pub(super) fn validate_rate_limits(&self, errors: &mut Vec<String>) {
166        if self.rate_limits.disabled {
167            return;
168        }
169
170        if self.rate_limits.burst_multiplier == 0 {
171            errors.push("rate_limits.burst_multiplier must be greater than 0".to_string());
172        }
173
174        Self::validate_rate_limit(
175            errors,
176            "oauth_public",
177            self.rate_limits.oauth_public_per_second,
178        );
179        Self::validate_rate_limit(errors, "oauth_auth", self.rate_limits.oauth_auth_per_second);
180        Self::validate_rate_limit(errors, "contexts", self.rate_limits.contexts_per_second);
181        Self::validate_rate_limit(errors, "tasks", self.rate_limits.tasks_per_second);
182        Self::validate_rate_limit(errors, "artifacts", self.rate_limits.artifacts_per_second);
183        Self::validate_rate_limit(errors, "agents", self.rate_limits.agents_per_second);
184        Self::validate_rate_limit(errors, "mcp", self.rate_limits.mcp_per_second);
185        Self::validate_rate_limit(errors, "stream", self.rate_limits.stream_per_second);
186        Self::validate_rate_limit(errors, "content", self.rate_limits.content_per_second);
187    }
188
189    fn validate_rate_limit(errors: &mut Vec<String>, name: &str, value: u64) {
190        if value == 0 {
191            errors.push(format!(
192                "rate_limits.{}_per_second must be greater than 0",
193                name
194            ));
195        }
196    }
197}