diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index f6b8715..298926b 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -8,6 +8,7 @@ use crate::ui::handlers::rat_state::UiStateHandler; use crate::ui::handlers::context::UiContext; use crate::ui::handlers::context::DialogPurpose; use crate::functions::common::buffer; +use std::error::Error; use crate::tui::{ terminal::core::TerminalCore, functions::{ @@ -41,6 +42,9 @@ use crate::modes::{ }; use crate::functions::modes::navigation::{admin_nav, add_table_nav}; use crate::config::binds::key_sequences::KeySequenceTracker; +use tokio::spawn; +use tokio::sync::mpsc; +use crate::tui::functions::common::login::LoginResult; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EventOutcome { @@ -60,10 +64,11 @@ pub struct EventHandler { pub ideal_cursor_column: usize, pub key_sequence_tracker: KeySequenceTracker, pub auth_client: AuthClient, + pub login_result_sender: mpsc::Sender, } impl EventHandler { - pub async fn new() -> Result> { + pub async fn new(login_result_sender: mpsc::Sender) -> Result> { Ok(EventHandler { command_mode: false, command_input: String::new(), @@ -74,6 +79,7 @@ impl EventHandler { ideal_cursor_column: 0, key_sequence_tracker: KeySequenceTracker::new(800), auth_client: AuthClient::new().await?, + login_result_sender, }) } @@ -222,14 +228,47 @@ impl EventHandler { } UiContext::Login => { let login_action_message = match index { - 0 => { // Index 0 corresponds to the "Login" button - match login::initiate_login(app_state, login_state).await { - Ok(outcome) => return Ok(outcome), - Err(e) => { - app_state.show_dialog("Error", &format!("Failed to initiate login: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); - login_state.login_request_pending = false; - "Error initiating login".to_string() - } + 0 => { // "Login" button pressed + let username = login_state.username.clone(); + let password = login_state.password.clone(); + + // 1. Client-side validation + if username.trim().is_empty() { + app_state.show_dialog( + "Login Failed", + "Username/Email cannot be empty.", + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + // Return a message, no need to modify login_state here + // as it will be cleared when result is processed + "Username cannot be empty.".to_string() + } else { + // 2. Show Loading Dialog + app_state.show_loading_dialog("Logging In", "Please wait..."); + + // 3. Clone sender for the task (needs sender from ui.rs) + // NOTE: We need access to login_result_sender here. + // This requires passing it into EventHandler or handle_event. + // Let's assume it's added to EventHandler state for now. + let sender = self.login_result_sender.clone(); // Assumes sender is part of EventHandler state + + // 4. Spawn the login task + spawn(async move { + let login_outcome = match AuthClient::new().await { + Ok(mut auth_client) => { + match auth_client.login(username, password).await { + Ok(response) => login::LoginResult::Success(response), + Err(e) => login::LoginResult::Failure(format!("{}", e)), + } + } + Err(e) => login::LoginResult::ConnectionError(format!("Failed to create AuthClient: {}", e)), + }; + let _ = sender.send(login_outcome).await; // Handle error? + }); + + // 5. Return immediately + "Login initiated.".to_string() } }, 1 => login::back_to_main(login_state, app_state, buffer_state).await, diff --git a/client/src/services/auth.rs b/client/src/services/auth.rs index a37f531..adcc3f4 100644 --- a/client/src/services/auth.rs +++ b/client/src/services/auth.rs @@ -5,13 +5,14 @@ use common::proto::multieko2::auth::{ LoginRequest, LoginResponse, RegisterRequest, AuthResponse, }; +use std::error::Error; pub struct AuthClient { client: AuthServiceClient, } impl AuthClient { - pub async fn new() -> Result> { + pub async fn new() -> Result> { let client = AuthServiceClient::connect("http://[::1]:50051").await?; Ok(Self { client }) } diff --git a/client/src/tui/functions/common/login.rs b/client/src/tui/functions/common/login.rs index 206c5f4..534660b 100644 --- a/client/src/tui/functions/common/login.rs +++ b/client/src/tui/functions/common/login.rs @@ -7,7 +7,15 @@ use crate::state::app::state::AppState; use crate::state::app::buffer::{AppView, BufferState}; use crate::state::pages::canvas_state::CanvasState; use crate::ui::handlers::context::DialogPurpose; -use crate::modes::handlers::event::EventOutcome; // Make sure this is imported +use crate::modes::handlers::event::EventOutcome; +use common::proto::multieko2::auth::LoginResponse; + +#[derive(Debug)] +pub enum LoginResult { + Success(LoginResponse), + Failure(String), + ConnectionError(String), +} /// Attempts to log the user in using the provided credentials via gRPC. /// Updates AuthState and AppState on success or failure. diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 9875c4e..3065701 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -22,20 +22,26 @@ use crate::ui::handlers::context::DialogPurpose; // <-- Add DialogPurpose import // Import SaveOutcome use crate::tui::terminal::{EventReader, TerminalCore}; use crate::ui::handlers::render::render_ui; -use crate::tui::functions::common::login; // <-- Add login module import +use crate::tui::functions::common::login; +use crate::tui::functions::common::login::LoginResult; use std::time::Instant; +use std::error::Error; use crossterm::cursor::SetCursorStyle; use crossterm::event as crossterm_event; use tracing::{info, error}; +use tokio::sync::mpsc; -pub async fn run_ui() -> Result<(), Box> { +pub async fn run_ui() -> Result<(), Box> { let config = Config::load()?; let theme = Theme::from_str(&config.colors.theme); let mut terminal = TerminalCore::new()?; let mut grpc_client = GrpcClient::new().await?; let mut command_handler = CommandHandler::new(); - let mut event_handler = EventHandler::new().await?; + // --- Channel for Login Results --- + let (login_result_sender, mut login_result_receiver) = + mpsc::channel::(1); + let mut event_handler = EventHandler::new(login_result_sender.clone()).await?; let event_reader = EventReader::new(); let mut auth_state = AuthState::default(); @@ -136,47 +142,6 @@ pub async fn run_ui() -> Result<(), Box> { } // --- End Cursor Visibility Logic --- - // --- 2. Check for Pending Login Action --- - if login_state.login_request_pending { - // Reset the flag *before* calling save - login_state.login_request_pending = false; - - // Create AuthClient and call save - match AuthClient::new().await { - Ok(mut auth_client_instance) => { - // Call the ORIGINAL save function from the login module - let save_result = login::save( - &mut auth_state, - &mut login_state, - &mut auth_client_instance, // Pass the new client instance - &mut app_state, - ).await; - - // Use tracing for logging the outcome - match save_result { - // save returns Result, Ok contains the message - Ok(msg) => info!(message = %msg, "Login save result"), // Use tracing::info! - Err(e) => error!(error = %e, "Error during login save"), // Use tracing::error! - } - // Note: save already handles showing the final dialog (success/failure) - } - Err(e) => { - // Handle client connection error - show dialog directly - // Ensure flag is already false here - app_state.show_dialog( // Use show_dialog, not update_dialog_content - "Login Failed", - &format!("Connection Error: {}", e), - vec!["OK".to_string()], - DialogPurpose::LoginFailed, // Use appropriate purpose - ); - login_state.error_message = Some(format!("Connection Error: {}", e)); - error!(error = %e, "Failed to create AuthClient"); // Use tracing::error! - } - } - // After save runs, the state (dialog content, etc.) is updated. - // The *next* iteration's draw call will show the final result. - } // --- End Pending Login Check --- - let total_count = app_state.total_count; let mut current_position = app_state.current_position; let position_before_event = current_position; @@ -211,6 +176,63 @@ pub async fn run_ui() -> Result<(), Box> { // This happens *after* the event is handled app_state.current_position = current_position; + // --- Check for Login Results from Channel --- + match login_result_receiver.try_recv() { + Ok(login_result) => { + // A result arrived from the login task! + match login_result { + LoginResult::Success(response) => { + // Update AuthState + auth_state.auth_token = Some(response.access_token.clone()); + auth_state.user_id = Some(response.user_id.clone()); + auth_state.role = Some(response.role.clone()); + auth_state.decoded_username = Some(response.username.clone()); + + // Update Dialog + let success_message = format!( + "Login Successful!\n\nUsername: {}\nUser ID: {}\nRole: {}", + response.username, response.user_id, response.role + ); + // Use update_dialog_content if loading dialog is shown, otherwise show_dialog + app_state.update_dialog_content( // Assuming loading dialog was shown + &success_message, + vec!["Menu".to_string(), "Exit".to_string()], + DialogPurpose::LoginSuccess, + ); + info!(message = %success_message, "Login successful"); + } + LoginResult::Failure(err_msg) => { + app_state.update_dialog_content( // Update loading dialog + &err_msg, + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + login_state.error_message = Some(err_msg.clone()); // Keep error message + error!(error = %err_msg, "Login failed"); + } + LoginResult::ConnectionError(err_msg) => { + app_state.update_dialog_content( // Update loading dialog + &err_msg, // Show connection error + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + login_state.error_message = Some(err_msg.clone()); + error!(error = %err_msg, "Login connection error"); + } + } + // Clear login form state regardless of outcome now that it's processed + login_state.username.clear(); + login_state.password.clear(); + login_state.set_has_unsaved_changes(false); + login_state.current_cursor_pos = 0; + } + Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ } + Err(mpsc::error::TryRecvError::Disconnected) => { + error!("Login result channel disconnected unexpectedly."); + // Optionally show an error dialog here + } + } + // --- Centralized Consequence Handling --- let mut should_exit = false; match event_outcome_result {