systemprompt_cloud/tenants/
mod.rs1mod tenant_store;
10
11use serde::{Deserialize, Serialize};
12use systemprompt_identifiers::TenantId;
13use validator::Validate;
14
15use crate::api_client::TenantInfo;
16
17pub use tenant_store::TenantStore;
18
19#[derive(Debug)]
20pub struct NewCloudTenantParams {
21 pub id: TenantId,
22 pub name: String,
23 pub app_id: Option<String>,
24 pub hostname: Option<String>,
25 pub region: Option<String>,
26 pub database_url: Option<String>,
27 pub internal_database_url: String,
28 pub external_db_access: bool,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
32#[serde(rename_all = "snake_case")]
33pub enum TenantType {
34 #[default]
35 Local,
36 Cloud,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
40pub struct StoredTenant {
41 pub id: TenantId,
42
43 #[validate(length(min = 1, message = "Tenant name cannot be empty"))]
44 pub name: String,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub app_id: Option<String>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub hostname: Option<String>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub region: Option<String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub database_url: Option<String>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub internal_database_url: Option<String>,
60
61 #[serde(default)]
62 pub tenant_type: TenantType,
63
64 #[serde(default)]
65 pub external_db_access: bool,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub docker_project: Option<String>,
69}
70
71impl StoredTenant {
72 #[must_use]
73 pub fn new(id: TenantId, name: String) -> Self {
74 Self {
75 id,
76 name,
77 app_id: None,
78 hostname: None,
79 region: None,
80 database_url: None,
81 internal_database_url: None,
82 tenant_type: TenantType::default(),
83 external_db_access: false,
84 docker_project: None,
85 }
86 }
87
88 #[must_use]
89 pub const fn new_local(id: TenantId, name: String, database_url: String) -> Self {
90 Self {
91 id,
92 name,
93 app_id: None,
94 hostname: None,
95 region: None,
96 database_url: Some(database_url),
97 internal_database_url: None,
98 tenant_type: TenantType::Local,
99 external_db_access: false,
100 docker_project: None,
101 }
102 }
103
104 #[must_use]
105 pub const fn new_local_docker(
106 id: TenantId,
107 name: String,
108 database_url: String,
109 docker_project: String,
110 ) -> Self {
111 Self {
112 id,
113 name,
114 app_id: None,
115 hostname: None,
116 region: None,
117 database_url: Some(database_url),
118 internal_database_url: None,
119 tenant_type: TenantType::Local,
120 external_db_access: false,
121 docker_project: Some(docker_project),
122 }
123 }
124
125 #[must_use]
126 pub fn new_cloud(params: NewCloudTenantParams) -> Self {
127 Self {
128 id: params.id,
129 name: params.name,
130 app_id: params.app_id,
131 hostname: params.hostname,
132 region: params.region,
133 database_url: params.database_url,
134 internal_database_url: Some(params.internal_database_url),
135 tenant_type: TenantType::Cloud,
136 external_db_access: params.external_db_access,
137 docker_project: None,
138 }
139 }
140
141 #[must_use]
142 pub fn from_tenant_info(info: &TenantInfo) -> Self {
143 Self {
144 id: TenantId::new(info.id.clone()),
145 name: info.name.clone(),
146 app_id: info.app_id.clone(),
147 hostname: info.hostname.clone(),
148 region: info.region.clone(),
149 database_url: None,
150 internal_database_url: Some(info.database_url.clone()),
151 tenant_type: TenantType::Cloud,
152 external_db_access: info.external_db_access,
153 docker_project: None,
154 }
155 }
156
157 #[must_use]
158 pub const fn uses_managed_container(&self) -> bool {
159 self.docker_project.is_some()
160 }
161
162 #[must_use]
163 pub fn has_database_url(&self) -> bool {
164 match self.tenant_type {
165 TenantType::Cloud => self
166 .internal_database_url
167 .as_ref()
168 .is_some_and(|url| !url.is_empty()),
169 TenantType::Local => self
170 .database_url
171 .as_ref()
172 .is_some_and(|url| !url.is_empty()),
173 }
174 }
175
176 #[must_use]
177 pub fn get_local_database_url(&self) -> Option<&String> {
178 self.database_url
179 .as_ref()
180 .or(self.internal_database_url.as_ref())
181 }
182
183 #[must_use]
184 pub const fn is_cloud(&self) -> bool {
185 matches!(self.tenant_type, TenantType::Cloud)
186 }
187
188 #[must_use]
189 pub const fn is_local(&self) -> bool {
190 matches!(self.tenant_type, TenantType::Local)
191 }
192
193 pub fn update_from_tenant_info(&mut self, info: &TenantInfo) {
194 self.name.clone_from(&info.name);
195 self.app_id.clone_from(&info.app_id);
196 self.hostname.clone_from(&info.hostname);
197 self.region.clone_from(&info.region);
198 self.external_db_access = info.external_db_access;
199
200 if !info.database_url.contains(":***@") {
201 self.internal_database_url = Some(info.database_url.clone());
202 }
203 }
204
205 #[must_use]
206 pub fn is_database_url_masked(&self) -> bool {
207 self.internal_database_url
208 .as_ref()
209 .is_some_and(|url| url.contains(":***@") || url.contains(":********@"))
210 }
211
212 #[must_use]
213 pub fn has_missing_credentials(&self) -> bool {
214 self.tenant_type == TenantType::Cloud && self.is_database_url_masked()
215 }
216}