1use crate::{
6 auth_n::email::send_invitation_email,
7 config::ServerConfig,
8 fhir_client::{FHIRServerClient, ServerCTX},
9 services::ServerState,
10};
11use email_address::EmailAddress;
12use haste_fhir_client::FHIRClient;
13use haste_fhir_model::r4::generated::{
14 resources::{Resource, ResourceType, User as UserResource},
15 terminology::IssueType,
16};
17use haste_fhir_operation_error::OperationOutcomeError;
18use haste_fhir_search::SearchEngine;
19use haste_fhir_terminology::FHIRTerminology;
20use haste_jwt::{ProjectId, TenantId};
21use haste_repository::{
22 Repository,
23 admin::{ProjectModelAdmin, TenantModelAdmin},
24 types::{
25 authorization_code::{AuthorizationCodeKind, CreateAuthorizationCode},
26 tenant::CreateTenant,
27 user::{CreateUser, UpdateUser, User},
28 },
29};
30use std::{str::FromStr, sync::Arc, time::Duration};
31
32pub const INVITATION_VALID_FOR: Duration = Duration::from_secs(7 * 24 * 60 * 60);
34
35pub fn resource_email_verified(user: &UserResource) -> bool {
37 user.emailVerified
38 .as_ref()
39 .and_then(|verified| verified.value)
40 .unwrap_or(false)
41}
42
43pub fn set_resource_email_verified(user: &mut UserResource, verified: bool) {
45 user.emailVerified.get_or_insert_default().value = Some(verified);
46}
47
48pub async fn tenant_display_name<Repo: Repository>(repo: &Repo, tenant: &TenantId) -> String {
50 TenantModelAdmin::<CreateTenant, _, _, _, _>::read(
51 repo,
52 &TenantId::System,
53 &tenant.as_ref().to_string(),
54 )
55 .await
56 .ok()
57 .flatten()
58 .and_then(|tenant| tenant.display_name)
59 .unwrap_or_else(|| tenant.as_ref().to_string())
60}
61
62async fn email_invitation<Repo: Repository>(
63 repo: &Repo,
64 config: &ServerConfig,
65 tenant: &TenantId,
66 user_id: &str,
67 to: &EmailAddress,
68) -> Result<(), OperationOutcomeError> {
69 let code = ProjectModelAdmin::create(
70 repo,
71 tenant,
72 &ProjectId::System,
73 CreateAuthorizationCode {
74 membership: None,
75 expires_in: INVITATION_VALID_FOR,
76 kind: AuthorizationCodeKind::Invitation,
77 user_id: user_id.to_string(),
78 client_id: None,
79 pkce_code_challenge: None,
80 pkce_code_challenge_method: None,
81 redirect_uri: None,
82 meta: None,
83 },
84 )
85 .await?;
86
87 let tenant_name = tenant_display_name(repo, tenant).await;
88
89 send_invitation_email(
90 config,
91 tenant,
92 &tenant_name,
93 to,
94 &code.code,
95 INVITATION_VALID_FOR,
96 )
97 .await
98}
99
100pub async fn send_invitation<Repo: Repository>(
103 repo: &Repo,
104 config: &ServerConfig,
105 tenant: &TenantId,
106 user: &UserResource,
107) {
108 let Some(user_id) = user.id.as_deref() else {
109 return;
110 };
111 let Some(email) = user.email.as_ref().and_then(|email| email.value.as_deref()) else {
112 return;
113 };
114 let Ok(to) = EmailAddress::from_str(email) else {
115 tracing::warn!(tenant = %tenant, user = user_id, "invited user has no valid email address");
116 return;
117 };
118
119 if let Err(error) = email_invitation(repo, config, tenant, user_id, &to).await {
120 tracing::warn!(tenant = %tenant, user = user_id, ?error, "invitation email not sent");
121 }
122}
123
124pub async fn find_invitation<Repo: Repository>(
126 repo: &Repo,
127 tenant: &TenantId,
128 code: &str,
129) -> Result<Option<User>, OperationOutcomeError> {
130 let Some(code) = ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::read(
131 repo,
132 tenant,
133 &ProjectId::System,
134 &code.to_string(),
135 )
136 .await?
137 else {
138 return Ok(None);
139 };
140
141 if code.kind != AuthorizationCodeKind::Invitation || code.is_expired.unwrap_or(true) {
142 return Ok(None);
143 }
144
145 TenantModelAdmin::<CreateUser, _, _, _, _>::read(repo, tenant, &code.user_id).await
146}
147
148pub async fn delete_invitation_code<Repo: Repository>(
150 repo: &Repo,
151 tenant: &TenantId,
152 code: &str,
153) -> Result<(), OperationOutcomeError> {
154 ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
155 repo,
156 tenant,
157 &ProjectId::System,
158 &code.to_string(),
159 )
160 .await
161}
162
163fn system_ctx<
164 Repo: Repository + Send + Sync + 'static,
165 Search: SearchEngine + Send + Sync + 'static,
166 Terminology: FHIRTerminology + Send + Sync + 'static,
167>(
168 state: &ServerState<Repo, Search, Terminology>,
169 tenant: &TenantId,
170) -> Arc<ServerCTX<FHIRServerClient<Repo, Search, Terminology>>> {
171 Arc::new(ServerCTX::system(
172 tenant.clone(),
173 ProjectId::System,
174 state.fhir_client.clone(),
175 state.rate_limit.clone(),
176 ))
177}
178
179pub async fn accept<
182 Repo: Repository + Send + Sync + 'static,
183 Search: SearchEngine + Send + Sync + 'static,
184 Terminology: FHIRTerminology + Send + Sync + 'static,
185>(
186 state: &ServerState<Repo, Search, Terminology>,
187 user: User,
188) -> Result<User, OperationOutcomeError> {
189 if user.email_verified {
190 return Ok(user);
191 }
192
193 let ctx = system_ctx(state, &user.tenant);
194
195 match state
196 .fhir_client
197 .read(ctx.clone(), ResourceType::User, user.id.clone())
198 .await?
199 {
200 Some(Resource::User(mut resource)) => {
201 set_resource_email_verified(&mut resource, true);
202 state
203 .fhir_client
204 .update(
205 ctx,
206 ResourceType::User,
207 user.id.clone(),
208 Resource::User(resource),
209 )
210 .await?;
211 }
212 _ => {
213 TenantModelAdmin::<CreateUser, _, _, _, String>::update(
214 state.repo.as_ref(),
215 &user.tenant,
216 UpdateUser {
217 id: user.id.clone(),
218 email: None,
219 role: None,
220 method: None,
221 provider_id: None,
222 password: None,
223 email_verified: Some(true),
224 },
225 )
226 .await?;
227 }
228 }
229
230 Ok(User {
231 email_verified: true,
232 ..user
233 })
234}
235
236pub async fn decline<
239 Repo: Repository + Send + Sync + 'static,
240 Search: SearchEngine + Send + Sync + 'static,
241 Terminology: FHIRTerminology + Send + Sync + 'static,
242>(
243 state: &ServerState<Repo, Search, Terminology>,
244 user: &User,
245) -> Result<(), OperationOutcomeError> {
246 if user.email_verified {
247 return Err(OperationOutcomeError::error(
248 IssueType::invalid(),
249 "That account was accepted already. Ask the workspace to remove it.".to_string(),
250 ));
251 }
252
253 let ctx = system_ctx(state, &user.tenant);
254
255 match state
256 .fhir_client
257 .read(ctx.clone(), ResourceType::User, user.id.clone())
258 .await?
259 {
260 Some(Resource::User(_)) => {
261 state
262 .fhir_client
263 .delete_instance(ctx, ResourceType::User, user.id.clone())
264 .await?;
265 }
266 _ => {
267 TenantModelAdmin::<CreateUser, _, _, _, _>::delete(
268 state.repo.as_ref(),
269 &user.tenant,
270 &user.id,
271 )
272 .await?;
273 }
274 }
275
276 Ok(())
277}