1use clap::{Subcommand, ValueEnum};
2use haste_config::{Config, get_config};
3use haste_fhir_client::FHIRClient;
4use haste_fhir_model::r4::generated::{
5 resources::{
6 AccessPolicyV2, AccessPolicyV2Target, Bundle, BundleEntry, BundleEntryRequest,
7 ClientApplication, Resource,
8 },
9 terminology::{
10 AccessPolicyv2Engine, BundleType, ClientapplicationGrantType,
11 ClientapplicationResponseTypes, HttpVerb, IssueType, UserRole,
12 },
13 types::{FHIRString, FHIRUri, Reference},
14};
15use haste_fhir_operation_error::OperationOutcomeError;
16use haste_fhir_search::SearchEngine;
17use haste_jwt::{ProjectId, TenantId, claims::SubscriptionTier};
18use haste_repository::admin::Migrate;
19use haste_server::{
20 ServerEnvironmentVariables,
21 fhir_client::ServerCTX,
22 load_artifacts, services,
23 tenants::{create_tenant, create_user},
24};
25use std::sync::Arc;
26
27#[derive(Clone, Debug, ValueEnum)]
28pub enum UserSubscriptionChoice {
29 Free,
30 Professional,
31 Team,
32 Unlimited,
33}
34
35impl From<UserSubscriptionChoice> for SubscriptionTier {
36 fn from(choice: UserSubscriptionChoice) -> Self {
37 match choice {
38 UserSubscriptionChoice::Free => SubscriptionTier::Free,
39 UserSubscriptionChoice::Professional => SubscriptionTier::Professional,
40 UserSubscriptionChoice::Team => SubscriptionTier::Team,
41 UserSubscriptionChoice::Unlimited => SubscriptionTier::Unlimited,
42 }
43 }
44}
45
46#[derive(Subcommand, Debug)]
47pub enum ClientCommands {
48 Create {
49 #[arg(short, long)]
50 id: String,
51 #[arg(short, long)]
52 secret: String,
53 #[arg(short, long)]
54 tenant: String,
55 #[arg(short, long)]
56 project: String,
57 },
58}
59
60#[derive(Subcommand, Debug)]
61pub enum AdminCommands {
62 Tenant {
63 #[command(subcommand)]
64 command: TenantCommands,
65 },
66
67 User {
68 #[command(subcommand)]
69 command: UserCommands,
70 },
71
72 Client {
73 #[command(subcommand)]
74 command: ClientCommands,
75 },
76
77 Migrate {
78 #[command(subcommand)]
79 command: MigrationCommands,
80 },
81}
82
83#[derive(Subcommand, Debug)]
84pub enum MigrationCommands {
85 Artifacts {},
86 Repo {},
87 Search {},
88 All,
89}
90
91#[derive(Subcommand, Debug)]
92pub enum TenantCommands {
93 Create {
94 #[arg(short, long)]
95 id: String,
96 #[arg(short, long)]
97 subscription_tier: Option<UserSubscriptionChoice>,
98 #[arg(long)]
99 owner_email: String,
100 #[arg(long)]
101 owner_password: String,
102 },
103}
104
105#[derive(Subcommand, Debug)]
106pub enum UserCommands {
107 Create {
108 #[arg(short, long)]
109 email: String,
110 #[arg(short, long)]
111 password: String,
112 #[arg(short, long)]
113 tenant: String,
114 },
115}
116
117async fn migrate_repo(
118 config: Arc<dyn Config<ServerEnvironmentVariables>>,
119) -> Result<(), OperationOutcomeError> {
120 let services = services::create_services(config).await?;
121 services.repo.migrate().await?;
122 Ok(())
123}
124
125async fn migrate_search(
126 config: Arc<dyn Config<ServerEnvironmentVariables>>,
127) -> Result<(), OperationOutcomeError> {
128 let services = services::create_services(config).await?;
129 services
130 .search
131 .migrate(&haste_repository::types::SupportedFHIRVersions::R4)
132 .await?;
133 Ok(())
134}
135
136async fn migrate_artifacts(
137 config: Arc<dyn Config<ServerEnvironmentVariables>>,
138) -> Result<(), OperationOutcomeError> {
139 let initial = config
140 .get(ServerEnvironmentVariables::AllowArtifactMutations)
141 .unwrap_or("false".to_string());
142 config.set(
143 ServerEnvironmentVariables::AllowArtifactMutations,
144 "true".to_string(),
145 )?;
146 load_artifacts::load_artifacts(config.clone()).await?;
147 config.set(ServerEnvironmentVariables::AllowArtifactMutations, initial)?;
148 Ok(())
149}
150
151pub async fn admin(command: &AdminCommands) -> Result<(), OperationOutcomeError> {
152 let config = get_config::<ServerEnvironmentVariables>("environment".into());
153
154 match &command {
155 AdminCommands::Migrate { command } => match command {
156 MigrationCommands::Artifacts {} => migrate_artifacts(config).await,
157 MigrationCommands::Repo {} => migrate_repo(config).await,
158 MigrationCommands::Search {} => migrate_search(config).await,
159 MigrationCommands::All => {
160 migrate_repo(config.clone()).await?;
161 migrate_search(config.clone()).await?;
162 migrate_artifacts(config).await?;
163 Ok(())
164 }
165 },
166 AdminCommands::Tenant { command } => match command {
167 TenantCommands::Create {
168 id,
169 subscription_tier,
170 owner_email,
171 owner_password,
172 } => {
173 let services = services::create_services(config).await?;
174 let result = create_tenant(
175 services.as_ref(),
176 Some(id.clone()),
177 id,
178 &SubscriptionTier::from(
179 subscription_tier
180 .clone()
181 .unwrap_or(UserSubscriptionChoice::Free),
182 ),
183 haste_fhir_model::r4::generated::resources::User {
184 role: Box::new(UserRole::Owner(None)),
185 email: Some(Box::new(
186 haste_fhir_model::r4::generated::types::FHIRString {
187 value: Some(owner_email.clone()),
188 ..Default::default()
189 },
190 )),
191 ..Default::default()
192 },
193 Some(owner_password),
194 )
195 .await;
196
197 if let Err(operation_outcome_error) = result.as_ref()
198 && let Some(issue) = operation_outcome_error.outcome().issue.first()
199 && matches!(issue.code.as_ref(), IssueType::Duplicate(None))
200 {
201 println!("Tenant with ID '{}' already exists.", id);
202 return Ok(());
203 }
204
205 result?;
206
207 Ok(())
208 }
209 },
210 AdminCommands::User { command } => match command {
211 UserCommands::Create {
212 email,
213 password,
214 tenant,
215 } => {
216 let services = services::create_services(config)
217 .await?
218 .transaction()
219 .await?;
220
221 let tenant = TenantId::new(tenant.clone());
222
223 create_user(
224 &services,
225 &tenant,
226 haste_fhir_model::r4::generated::resources::User {
227 role: Box::new(UserRole::Admin(None)),
228 email: Some(Box::new(
229 haste_fhir_model::r4::generated::types::FHIRString {
230 value: Some(email.clone()),
231 ..Default::default()
232 },
233 )),
234 ..Default::default()
235 },
236 Some(password),
237 )
238 .await?;
239
240 services.commit().await?;
241
242 Ok(())
243 }
244 },
245 AdminCommands::Client { command } => match command {
246 ClientCommands::Create {
247 tenant,
248 project,
249 id,
250 secret,
251 } => {
252 let services = services::create_services(config).await?;
253
254 let ctx = Arc::new(ServerCTX::system(
255 TenantId::new(tenant.clone()),
256 ProjectId::new(project.clone()),
257 services.fhir_client.clone(),
258 services.rate_limit.clone(),
259 ));
260
261 let transaction_bundle = Bundle {
262 type_: Box::new(BundleType::Transaction(None)),
263 entry: Some(vec![
264 BundleEntry {
265 fullUrl: Some(Box::new(FHIRUri {
266 value: Some("access-policy".to_string()),
267 ..Default::default()
268 })),
269 request: Some(BundleEntryRequest {
270 method: Box::new(HttpVerb::POST(None)),
271 url: Box::new(FHIRUri {
272 value: Some("AccessPolicyV2".to_string()),
273 ..Default::default()
274 }),
275 ..Default::default()
276 }),
277 resource: Some(Box::new(Resource::AccessPolicyV2(AccessPolicyV2 {
278 name: Box::new(FHIRString {
279 value: Some("ADMIN".to_string()),
280 ..Default::default()
281 }),
282 engine: Box::new(AccessPolicyv2Engine::FullAccess(None)),
283 target: Some(vec![AccessPolicyV2Target {
284 link: Box::new(Reference {
285 reference: Some(Box::new(FHIRString {
286 value: Some("client-app".to_string()),
287 ..Default::default()
288 })),
289 ..Default::default()
290 }),
291 }]),
292 ..Default::default()
293 }))),
294 ..Default::default()
295 },
296 BundleEntry {
297 fullUrl: Some(Box::new(FHIRUri {
298 value: Some("client-app".to_string()),
299 ..Default::default()
300 })),
301 request: Some(BundleEntryRequest {
302 method: Box::new(HttpVerb::PUT(None)),
303 url: Box::new(FHIRUri {
304 value: Some(format!("ClientApplication/{}", id)),
305 ..Default::default()
306 }),
307 ..Default::default()
308 }),
309 resource: Some(Box::new(Resource::ClientApplication(
310 ClientApplication {
311 id: Some(id.clone()),
312 secret: Some(Box::new(FHIRString {
313 value: Some(secret.clone()),
314 ..Default::default()
315 })),
316
317 scope: Some(Box::new(FHIRString {
318 value: Some("openid system/*.*".to_string()),
319 ..Default::default()
320 })),
321
322 name: Box::new(FHIRString {
323 value: Some("CLI".to_string()),
324 ..Default::default()
325 }),
326
327 grantType: vec![Box::new(
328 ClientapplicationGrantType::Client_credentials(None),
329 )],
330
331 responseTypes: Box::new(ClientapplicationResponseTypes::Token(
332 None,
333 )),
334
335 ..Default::default()
336 },
337 ))),
338 ..Default::default()
339 },
340 ]),
341 ..Default::default()
342 };
343
344 services
345 .fhir_client
346 .transaction(ctx, transaction_bundle)
347 .await?;
348
349 Ok(())
350 }
351 },
352 }
353}