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