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