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