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