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