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