1use std::sync::Arc;
7
8use anyhow::Result;
9use colored::Colorize;
10use serde::Serialize;
11
12use raps_acc::admin::AccountAdminClient;
13use raps_acc::users::ProjectUsersClient;
14use raps_admin::{BulkConfig, bulk_add_user};
15use raps_kernel::auth::AuthClient;
16use raps_kernel::config::Config;
17use raps_kernel::http::HttpClientConfig;
18
19use crate::output::OutputFormat;
20
21use super::csv_ops::{execute_csv_import, execute_csv_update};
22use super::operations::display_bulk_result;
23use super::{
24 UserCommands, create_bulk_progress_bar, get_account_id, make_progress_callback,
25 parse_filter_with_ids,
26};
27
28#[derive(Serialize)]
29pub(crate) struct UserListOutput {
30 pub(crate) id: String,
31 pub(crate) email: String,
32 pub(crate) name: String,
33 pub(crate) role: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub(crate) company: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub(crate) status: Option<String>,
38}
39
40pub(crate) fn display_user_list(
41 users: &Vec<UserListOutput>,
42 output_format: OutputFormat,
43) -> Result<()> {
44 if users.is_empty() {
45 match output_format {
46 OutputFormat::Table => println!("{}", "No users found.".yellow()),
47 _ => output_format.write(&Vec::<UserListOutput>::new())?,
48 }
49 return Ok(());
50 }
51
52 match output_format {
53 OutputFormat::Table => {
54 println!("{}", "Users:".bold());
55 println!("{}", "\u{2500}".repeat(110));
56 println!(
57 "{:<30} {:<25} {:<18} {:<18} {}",
58 "Email".bold(),
59 "Name".bold(),
60 "Role".bold(),
61 "Status".bold(),
62 "Company".bold()
63 );
64 println!("{}", "\u{2500}".repeat(110));
65
66 for u in users {
67 let email_truncated = if u.email.len() > 28 {
68 format!("{}...", &u.email[..25])
69 } else {
70 u.email.clone()
71 };
72 let name_truncated = if u.name.len() > 23 {
73 format!("{}...", &u.name[..20])
74 } else {
75 u.name.clone()
76 };
77 let role_display = if u.role.is_empty() {
78 "-".to_string()
79 } else if u.role.len() > 16 {
80 format!("{}...", &u.role[..13])
81 } else {
82 u.role.clone()
83 };
84 let status_display = u.status.as_deref().unwrap_or("-");
85 let company_display = u.company.as_deref().unwrap_or("-");
86
87 println!(
88 "{:<30} {:<25} {:<18} {:<18} {}",
89 email_truncated.cyan(),
90 name_truncated,
91 role_display,
92 format_user_status(status_display),
93 company_display.dimmed()
94 );
95 }
96
97 println!("{}", "\u{2500}".repeat(110));
98 println!("{} {} user(s) found", "\u{2192}".cyan(), users.len());
99 }
100 _ => {
101 output_format.write(users)?;
102 }
103 }
104
105 Ok(())
106}
107
108pub(crate) fn format_user_status(status: &str) -> String {
109 match status.to_lowercase().as_str() {
110 "active" => status.green().to_string(),
111 "inactive" | "not_invited" => status.yellow().to_string(),
112 "disabled" => status.red().to_string(),
113 _ => status.to_string(),
114 }
115}
116
117impl UserCommands {
118 pub async fn execute(
119 self,
120 config: &Config,
121 auth_client: &AuthClient,
122 output_format: OutputFormat,
123 ) -> Result<()> {
124 match self {
125 UserCommands::List {
126 account,
127 project,
128 role,
129 status,
130 search,
131 } => {
132 let account_id = get_account_id(account)?;
133 let http_config = HttpClientConfig::default();
134
135 if let Some(project_id) = project {
136 if output_format.supports_colors() {
138 println!(
139 "\n{} List users in project {}",
140 "\u{2192}".cyan(),
141 project_id.cyan()
142 );
143 println!();
144 }
145
146 let users_client = ProjectUsersClient::new_with_http_config(
147 config.clone(),
148 auth_client.clone(),
149 http_config,
150 );
151
152 let all_users = users_client.list_all_project_users(&project_id).await?;
153
154 let filtered: Vec<_> = all_users
156 .into_iter()
157 .filter(|u| {
158 if let Some(ref r) = role {
159 if let Some(ref role_name) = u.role_name {
160 if !role_name.to_lowercase().contains(&r.to_lowercase()) {
161 return false;
162 }
163 } else {
164 return false;
165 }
166 }
167 if let Some(ref s) = search {
168 let s_lower = s.to_lowercase();
169 let email_match = u
170 .email
171 .as_ref()
172 .map(|e| e.to_lowercase().contains(&s_lower))
173 .unwrap_or(false);
174 let name_match = u
175 .name
176 .as_ref()
177 .map(|n| n.to_lowercase().contains(&s_lower))
178 .unwrap_or(false);
179 if !email_match && !name_match {
180 return false;
181 }
182 }
183 true
184 })
185 .collect();
186
187 let outputs: Vec<UserListOutput> = filtered
188 .iter()
189 .map(|u| UserListOutput {
190 id: u.id.clone(),
191 email: u.email.clone().unwrap_or_default(),
192 name: u.name.clone().unwrap_or_default(),
193 role: u.role_name.clone().unwrap_or_default(),
194 company: None,
195 status: None,
196 })
197 .collect();
198
199 display_user_list(&outputs, output_format)?;
200 } else {
201 if output_format.supports_colors() {
203 println!(
204 "\n{} List users in account {}",
205 "\u{2192}".cyan(),
206 account_id.cyan()
207 );
208 println!();
209 }
210
211 let admin_client = AccountAdminClient::new_with_http_config(
212 config.clone(),
213 auth_client.clone(),
214 http_config,
215 );
216
217 let all_users = admin_client.list_all_users(&account_id).await?;
218
219 let filtered: Vec<_> = all_users
221 .into_iter()
222 .filter(|u| {
223 if let Some(ref s) = status {
224 if let Some(ref user_status) = u.status {
225 if !user_status.to_lowercase().eq(&s.to_lowercase()) {
226 return false;
227 }
228 } else {
229 return false;
230 }
231 }
232 if let Some(ref s) = search {
233 let s_lower = s.to_lowercase();
234 let email_match = u.email.to_lowercase().contains(&s_lower);
235 let name_match = u
236 .name
237 .as_ref()
238 .map(|n| n.to_lowercase().contains(&s_lower))
239 .unwrap_or(false);
240 let first_match = u
241 .first_name
242 .as_ref()
243 .map(|n| n.to_lowercase().contains(&s_lower))
244 .unwrap_or(false);
245 let last_match = u
246 .last_name
247 .as_ref()
248 .map(|n| n.to_lowercase().contains(&s_lower))
249 .unwrap_or(false);
250 if !email_match && !name_match && !first_match && !last_match {
251 return false;
252 }
253 }
254 true
255 })
256 .collect();
257
258 let outputs: Vec<UserListOutput> = filtered
259 .iter()
260 .map(|u| {
261 let display_name = match (&u.first_name, &u.last_name) {
262 (Some(f), Some(l)) => format!("{} {}", f, l),
263 (Some(f), None) => f.clone(),
264 (None, Some(l)) => l.clone(),
265 (None, None) => u.name.clone().unwrap_or_default(),
266 };
267 UserListOutput {
268 id: u.id.clone(),
269 email: u.email.clone(),
270 name: display_name,
271 role: String::new(),
272 company: u.company_id.clone(),
273 status: u.status.clone(),
274 }
275 })
276 .collect();
277
278 display_user_list(&outputs, output_format)?;
279 }
280
281 Ok(())
282 }
283
284 UserCommands::Add {
285 email,
286 account,
287 role,
288 filter,
289 project_ids,
290 concurrency,
291 dry_run,
292 yes: _,
293 } => {
294 let account_id = get_account_id(account)?;
295 let project_filter = parse_filter_with_ids(&filter, &project_ids)?;
296
297 let bulk_config = BulkConfig {
298 concurrency: concurrency.min(50),
299 dry_run,
300 ..Default::default()
301 };
302
303 if output_format.supports_colors() {
304 println!(
305 "\n{} Bulk add user: {} to account {}",
306 "\u{2192}".cyan(),
307 email.green(),
308 account_id.cyan()
309 );
310 if let Some(r) = &role {
311 println!(" Role: {}", r.yellow());
312 }
313 if let Some(f) = &filter {
314 println!(" Filter: {}", f);
315 }
316 println!(" Concurrency: {}", concurrency.min(50));
317 if dry_run {
318 println!(" {} Dry-run mode enabled", "\u{26A0}".yellow());
319 }
320 println!();
321 }
322
323 let http_config = HttpClientConfig::default();
325 let admin_client = AccountAdminClient::new_with_http_config(
326 config.clone(),
327 auth_client.clone(),
328 http_config.clone(),
329 );
330 let users_client = Arc::new(ProjectUsersClient::new_with_http_config(
331 config.clone(),
332 auth_client.clone(),
333 http_config,
334 ));
335
336 let progress_bar = create_bulk_progress_bar(output_format);
337 let on_progress = make_progress_callback(progress_bar.clone());
338
339 let result = bulk_add_user(
340 &admin_client,
341 users_client,
342 &account_id,
343 &email,
344 role.as_deref(),
345 &project_filter,
346 bulk_config,
347 on_progress,
348 )
349 .await?;
350
351 if let Some(pb) = progress_bar {
353 pb.finish_and_clear();
354 }
355
356 display_bulk_result(&result, output_format)?;
358
359 if result.failed > 0 {
361 anyhow::bail!(
362 "Bulk operation partially failed: {} items failed",
363 result.failed
364 );
365 }
366
367 Ok(())
368 }
369
370 UserCommands::Remove {
371 email,
372 account,
373 filter,
374 project_ids,
375 concurrency,
376 dry_run,
377 yes: _,
378 } => {
379 let account_id = get_account_id(account)?;
380 let project_filter = parse_filter_with_ids(&filter, &project_ids)?;
381
382 let bulk_config = BulkConfig {
383 concurrency: concurrency.min(50),
384 dry_run,
385 ..Default::default()
386 };
387
388 if output_format.supports_colors() {
389 println!(
390 "\n{} Bulk remove user: {} from account {}",
391 "\u{2192}".cyan(),
392 email.red(),
393 account_id.cyan()
394 );
395 if let Some(f) = &filter {
396 println!(" Filter: {}", f);
397 }
398 println!(" Concurrency: {}", concurrency.min(50));
399 if dry_run {
400 println!(" {} Dry-run mode enabled", "\u{26A0}".yellow());
401 }
402 println!();
403 }
404
405 let http_config = HttpClientConfig::default();
407 let admin_client = AccountAdminClient::new_with_http_config(
408 config.clone(),
409 auth_client.clone(),
410 http_config.clone(),
411 );
412 let users_client = Arc::new(ProjectUsersClient::new_with_http_config(
413 config.clone(),
414 auth_client.clone(),
415 http_config,
416 ));
417
418 let progress_bar = create_bulk_progress_bar(output_format);
419 let on_progress = make_progress_callback(progress_bar.clone());
420
421 let result = raps_admin::bulk_remove_user(
422 &admin_client,
423 users_client,
424 &account_id,
425 &email,
426 &project_filter,
427 bulk_config,
428 on_progress,
429 )
430 .await?;
431
432 if let Some(pb) = progress_bar {
434 pb.finish_and_clear();
435 }
436
437 display_bulk_result(&result, output_format)?;
439
440 if result.failed > 0 {
442 anyhow::bail!(
443 "Bulk operation partially failed: {} items failed",
444 result.failed
445 );
446 }
447
448 Ok(())
449 }
450
451 UserCommands::Update {
452 email,
453 account,
454 role,
455 company,
456 from_role,
457 filter,
458 project_ids,
459 from_csv,
460 concurrency,
461 dry_run,
462 yes: _,
463 } => {
464 if let Some(csv_path) = from_csv {
466 return execute_csv_update(
467 config,
468 auth_client,
469 account.clone(),
470 filter.clone(),
471 project_ids.clone(),
472 &csv_path,
473 concurrency,
474 dry_run,
475 output_format,
476 )
477 .await;
478 }
479
480 if role.is_none() && company.is_none() {
482 anyhow::bail!("At least one of --role or --company must be provided.");
483 }
484
485 let account_id = get_account_id(account)?;
486
487 let http_config = HttpClientConfig::default();
488 let admin_client = AccountAdminClient::new_with_http_config(
489 config.clone(),
490 auth_client.clone(),
491 http_config.clone(),
492 );
493
494 if let Some(ref company_name) = company {
496 if output_format.supports_colors() {
497 println!(
498 "\n{} Update company for user: {} to: {}",
499 "\u{2192}".cyan(),
500 email.green(),
501 company_name.yellow()
502 );
503 if dry_run {
504 println!(" {} Dry-run mode enabled", "\u{26A0}".yellow());
505 }
506 }
507
508 if !dry_run {
509 let user = admin_client
511 .find_user_by_email(&account_id, &email)
512 .await?
513 .ok_or_else(|| anyhow::anyhow!("User not found: {}", email))?;
514
515 let update_req = raps_acc::admin::UpdateAccountUserRequest {
516 company_id: None,
517 company_name: Some(company_name.clone()),
518 };
519
520 admin_client
521 .update_user(&account_id, &user.id, update_req)
522 .await?;
523
524 if output_format.supports_colors() {
525 println!(
526 "{} Company updated for {} to '{}'",
527 "\u{2713}".green().bold(),
528 email,
529 company_name
530 );
531 }
532 } else if output_format.supports_colors() {
533 println!(
534 " {} Would update company for {} to '{}'",
535 "\u{2192}".dimmed(),
536 email,
537 company_name
538 );
539 }
540 }
541
542 if let Some(ref role_value) = role {
544 let project_filter = parse_filter_with_ids(&filter, &project_ids)?;
545
546 let bulk_config = BulkConfig {
547 concurrency: concurrency.min(50),
548 dry_run,
549 ..Default::default()
550 };
551
552 if output_format.supports_colors() {
553 println!(
554 "\n{} Bulk update user: {} to role: {}",
555 "\u{2192}".cyan(),
556 email.green(),
557 role_value.yellow()
558 );
559 if let Some(fr) = &from_role {
560 println!(" From role: {}", fr);
561 }
562 if let Some(f) = &filter {
563 println!(" Filter: {}", f);
564 }
565 println!(" Concurrency: {}", concurrency.min(50));
566 if dry_run {
567 println!(" {} Dry-run mode enabled", "\u{26A0}".yellow());
568 }
569 println!();
570 }
571
572 let users_client = Arc::new(ProjectUsersClient::new_with_http_config(
573 config.clone(),
574 auth_client.clone(),
575 http_config,
576 ));
577
578 let progress_bar = create_bulk_progress_bar(output_format);
579 let on_progress = make_progress_callback(progress_bar.clone());
580
581 let result = raps_admin::bulk_update_role(
582 &admin_client,
583 users_client,
584 &account_id,
585 &email,
586 role_value,
587 from_role.as_deref(),
588 &project_filter,
589 bulk_config,
590 on_progress,
591 )
592 .await?;
593
594 if let Some(pb) = progress_bar {
596 pb.finish_and_clear();
597 }
598
599 display_bulk_result(&result, output_format)?;
601
602 if result.failed > 0 {
604 anyhow::bail!(
605 "Bulk operation partially failed: {} items failed",
606 result.failed
607 );
608 }
609 }
610
611 Ok(())
612 }
613
614 UserCommands::AddToProject {
615 project,
616 email,
617 role_id,
618 } => {
619 let http_config = HttpClientConfig::default();
620 let users_client = ProjectUsersClient::new_with_http_config(
621 config.clone(),
622 auth_client.clone(),
623 http_config,
624 );
625
626 if output_format.supports_colors() {
627 println!(
628 "\n{} Adding user {} to project {}",
629 "\u{2192}".cyan(),
630 email.cyan(),
631 project.cyan()
632 );
633 }
634
635 let request = raps_acc::users::AddProjectUserRequest {
636 email: email.clone(),
637 role_id: role_id.clone(),
638 products: vec![],
639 };
640
641 let user = users_client.add_user(&project, request).await?;
642
643 #[derive(Serialize)]
644 struct AddResult {
645 user_id: String,
646 email: String,
647 role: Option<String>,
648 project: String,
649 }
650
651 let result = AddResult {
652 user_id: user.id,
653 email: user.email.unwrap_or(email),
654 role: user.role_name,
655 project,
656 };
657
658 output_format.write(&result)?;
659
660 if output_format.supports_colors() {
661 println!("\n{} User added successfully", "\u{2713}".green());
662 }
663
664 Ok(())
665 }
666
667 UserCommands::RemoveFromProject {
668 project,
669 user_id,
670 yes,
671 } => {
672 let http_config = HttpClientConfig::default();
673 let users_client = ProjectUsersClient::new_with_http_config(
674 config.clone(),
675 auth_client.clone(),
676 http_config,
677 );
678
679 if !yes && output_format.supports_colors() {
680 println!(
681 "\n{} Remove user {} from project {}?",
682 "\u{26A0}".yellow(),
683 user_id.cyan(),
684 project.cyan()
685 );
686 print!("Continue? [y/N] ");
687 use std::io::{self, Write};
688 io::stdout().flush()?;
689 let mut input = String::new();
690 io::stdin().read_line(&mut input)?;
691 if !input.trim().eq_ignore_ascii_case("y") {
692 println!("Cancelled.");
693 return Ok(());
694 }
695 }
696
697 users_client.remove_user(&project, &user_id).await?;
698
699 if output_format.supports_colors() {
700 println!(
701 "\n{} User {} removed from project {}",
702 "\u{2713}".green(),
703 user_id.cyan(),
704 project.cyan()
705 );
706 } else {
707 println!("User {} removed from project {}", user_id, project);
708 }
709
710 Ok(())
711 }
712
713 UserCommands::UpdateInProject {
714 project,
715 user_id,
716 role_id,
717 } => {
718 let http_config = HttpClientConfig::default();
719 let users_client = ProjectUsersClient::new_with_http_config(
720 config.clone(),
721 auth_client.clone(),
722 http_config,
723 );
724
725 if output_format.supports_colors() {
726 println!(
727 "\n{} Updating user {} in project {}",
728 "\u{2192}".cyan(),
729 user_id.cyan(),
730 project.cyan()
731 );
732 }
733
734 let request = raps_acc::users::UpdateProjectUserRequest {
735 role_id: role_id.clone(),
736 products: None,
737 };
738
739 let user = users_client
740 .update_user(&project, &user_id, request)
741 .await?;
742
743 #[derive(Serialize)]
744 struct UpdateResult {
745 user_id: String,
746 email: Option<String>,
747 role: Option<String>,
748 project: String,
749 }
750
751 let result = UpdateResult {
752 user_id: user.id,
753 email: user.email,
754 role: user.role_name,
755 project,
756 };
757
758 output_format.write(&result)?;
759
760 if output_format.supports_colors() {
761 println!("\n{} User updated successfully", "\u{2713}".green());
762 }
763
764 Ok(())
765 }
766
767 UserCommands::Import { project, from_csv } => {
768 execute_csv_import(config, auth_client, &project, &from_csv, output_format).await
769 }
770 }
771 }
772}