1use crate::{
2 auth_n::{invitations::set_resource_email_verified, oidc::utilities::set_user_password},
3 fhir_client::ServerCTX,
4 services::ServerState,
5 ui::components::TenantName,
6};
7use haste_fhir_client::FHIRClient;
8use haste_fhir_model::r4::generated::{
9 resources::{Project, Resource, ResourceType, User},
10 terminology::{IssueType, SupportedFhirVersion, UserRole},
11 types::FHIRString,
12};
13use haste_fhir_operation_error::OperationOutcomeError;
14use haste_fhir_search::SearchEngine;
15use haste_fhir_terminology::FHIRTerminology;
16use haste_jwt::{ProjectId, TenantId, claims::SubscriptionTier};
17use haste_repository::{
18 Repository,
19 admin::TenantModelAdmin,
20 types::{
21 tenant::{CreateTenant, Tenant},
22 user::CreateUser,
23 },
24 utilities::{HOSTNAME_ID_MIN_LEN, generate_hostname_id, validate_hostname_id},
25};
26use std::sync::Arc;
27
28pub async fn create_user<
29 Repo: Repository + Send + Sync + 'static,
30 Search: SearchEngine + Send + Sync + 'static,
31 Terminology: FHIRTerminology + Send + Sync + 'static,
32>(
33 services: &ServerState<Repo, Search, Terminology>,
34 tenant: &TenantId,
35 mut user_resource: User,
36 password: Option<&str>,
37) -> Result<User, OperationOutcomeError> {
38 if password.is_some() || user_resource.role == UserRole::owner() {
41 set_resource_email_verified(&mut user_resource, true);
42 }
43
44 let ctx = Arc::new(ServerCTX::system(
45 tenant.clone(),
46 ProjectId::System,
47 services.fhir_client.clone(),
48 services.rate_limit.clone(),
49 ));
50
51 let user = services
52 .fhir_client
53 .create(ctx, ResourceType::User, Resource::User(user_resource))
54 .await?;
55
56 let user = match user {
57 Resource::User(user) => user,
58 _ => panic!("Created resource is not a User"),
59 };
60
61 let user_id = user.id.clone().unwrap();
62
63 if let Some(password) = password {
64 set_user_password(
65 &*services.repo,
66 tenant,
67 &user
68 .email
69 .as_ref()
70 .and_then(|e| e.value.as_ref())
71 .map(|s| s.to_string())
72 .unwrap_or_default(),
73 &user_id,
74 password,
75 )
76 .await?;
77 }
78
79 Ok(user)
80}
81
82pub struct CreateTenantOutput {
83 pub tenant: Tenant,
84 pub owner: haste_repository::types::user::User,
85}
86
87pub async fn read_tenant<
88 Repo: Repository + Send + Sync + 'static,
89 Search: SearchEngine + Send + Sync + 'static,
90 Terminology: FHIRTerminology + Send + Sync + 'static,
91>(
92 services: &ServerState<Repo, Search, Terminology>,
93 tenant_id: &TenantId,
94) -> Result<Tenant, OperationOutcomeError> {
95 TenantModelAdmin::<CreateTenant, _, _, _, _>::read(
96 services.repo.as_ref(),
97 &TenantId::System,
98 &tenant_id.as_ref().to_string(),
99 )
100 .await?
101 .ok_or_else(|| {
102 OperationOutcomeError::error(
103 IssueType::not_found(),
104 format!("Tenant '{}' was not found.", tenant_id),
105 )
106 })
107}
108
109pub fn tenant_name(tenant: &Tenant) -> TenantName {
110 TenantName(tenant.display_name.clone())
111}
112
113pub const TENANT_ID_MIN_LEN: usize = HOSTNAME_ID_MIN_LEN;
114pub const TENANT_ID_MAX_LEN: usize = 32;
115
116const RESERVED_TENANT_IDS: &[&str] = &[
119 "admin",
120 "api",
121 "auth",
122 "www",
123 "haste",
124 "haste-health",
125 "hastehealth",
126 "login",
127 "signup",
128 "support",
129 "help",
130];
131
132const PUBLIC_MAIL_DOMAINS: &[&str] = &[
134 "gmail.com",
135 "googlemail.com",
136 "outlook.com",
137 "hotmail.com",
138 "live.com",
139 "msn.com",
140 "yahoo.com",
141 "ymail.com",
142 "icloud.com",
143 "me.com",
144 "mac.com",
145 "aol.com",
146 "proton.me",
147 "protonmail.com",
148 "pm.me",
149 "mail.com",
150 "gmx.com",
151 "gmx.net",
152 "fastmail.com",
153 "hey.com",
154 "yandex.com",
155 "zoho.com",
156];
157
158pub fn validate_tenant_id(id: &str) -> Result<TenantId, OperationOutcomeError> {
162 let invalid = |message: String| OperationOutcomeError::error(IssueType::invalid(), message);
163
164 let length = id.chars().count();
165 if !(TENANT_ID_MIN_LEN..=TENANT_ID_MAX_LEN).contains(&length) {
166 return Err(invalid(format!(
167 "A tenant id is {TENANT_ID_MIN_LEN} to {TENANT_ID_MAX_LEN} characters long."
168 )));
169 }
170
171 validate_hostname_id(id)?;
172
173 let tenant = TenantId::new(id.to_string());
174 if tenant == TenantId::System || RESERVED_TENANT_IDS.contains(&id) {
175 return Err(invalid(format!(
176 "'{id}' is reserved. Choose another tenant id."
177 )));
178 }
179
180 Ok(tenant)
181}
182
183fn random_tenant_id() -> TenantId {
184 TenantId::new(generate_hostname_id(Some(16)))
185}
186
187pub fn suggested_tenant_id(email: &str) -> String {
190 let (mailbox, domain) = email.rsplit_once('@').unwrap_or((email, ""));
191 let domain = domain.to_ascii_lowercase();
192 let base = if domain.is_empty() || PUBLIC_MAIL_DOMAINS.contains(&domain.as_str()) {
193 mailbox
194 } else {
195 domain.split('.').next().unwrap_or(mailbox)
196 };
197
198 let id = base
200 .to_ascii_lowercase()
201 .split(|character: char| !character.is_ascii_alphanumeric())
202 .filter(|part| !part.is_empty())
203 .collect::<Vec<_>>()
204 .join("-");
205 let id = id[..id.len().min(TENANT_ID_MAX_LEN)].trim_end_matches('-');
206
207 validate_tenant_id(id)
208 .unwrap_or_else(|_| random_tenant_id())
209 .as_ref()
210 .to_string()
211}
212
213pub fn suggested_tenant_id_variant(base: &str) -> String {
216 const SUFFIX_LEN: usize = 4;
217
218 let base: String = base
219 .chars()
220 .take(TENANT_ID_MAX_LEN - SUFFIX_LEN - 1)
221 .collect();
222 let base = base.trim_end_matches('-');
223
224 format!("{base}-{}", generate_hostname_id(Some(SUFFIX_LEN)))
225}
226
227pub async fn create_tenant<
228 Repo: Repository + Send + Sync + 'static,
229 Search: SearchEngine + Send + Sync + 'static,
230 Terminology: FHIRTerminology + Send + Sync + 'static,
231>(
232 services: &ServerState<Repo, Search, Terminology>,
233 tenant_id: Option<String>,
234 subscription_tier: &SubscriptionTier,
235 owner: haste_fhir_model::r4::generated::resources::User,
236 owner_password: Option<&str>,
237) -> Result<CreateTenantOutput, OperationOutcomeError> {
238 let id = match tenant_id {
239 Some(id) => validate_tenant_id(&id)?,
240 None => random_tenant_id(),
241 };
242
243 let services = services.transaction().await?;
244
245 let new_tenant = TenantModelAdmin::create(
246 &*services.repo,
247 &TenantId::System,
248 CreateTenant {
249 id: Some(id),
250 subscription_tier: Some(subscription_tier.clone().into()),
251 display_name: None,
252 logo_data: None,
253 logo_content_type: None,
254 },
255 )
256 .await?;
257
258 services
259 .fhir_client
260 .create(
261 Arc::new(ServerCTX::system(
262 new_tenant.id.clone(),
263 ProjectId::System,
264 services.fhir_client.clone(),
265 services.rate_limit.clone(),
266 )),
267 ResourceType::Project,
268 Resource::Project(Project {
269 id: Some(ProjectId::System.to_string()),
270 name: Box::new(FHIRString {
271 value: Some(ProjectId::System.to_string()),
272 ..Default::default()
273 }),
274 fhirVersion: SupportedFhirVersion::r4(),
275 ..Default::default()
276 }),
277 )
278 .await?;
279
280 let user = create_user(&services, &new_tenant.id, owner, owner_password).await?;
281
282 let Some(user_id) = user.id else {
283 return Err(OperationOutcomeError::fatal(
284 IssueType::invalid(),
285 "The user ID is required to complete the tenant creation process.".to_string(),
286 ));
287 };
288
289 let Some(user) = TenantModelAdmin::<CreateUser, _, _, _, _>::read(
290 services.repo.as_ref(),
291 &new_tenant.id,
292 &user_id,
293 )
294 .await?
295 else {
296 return Err(OperationOutcomeError::fatal(
297 IssueType::invalid(),
298 "The user does not exist after creation.".to_string(),
299 ));
300 };
301
302 services.commit().await?;
303
304 Ok(CreateTenantOutput {
305 tenant: new_tenant,
306 owner: user,
307 })
308}