// src/pages/login/logic.rs use crate::services::auth::AuthClient; use crate::state::pages::auth::AuthState; use crate::state::app::state::AppState; use crate::buffer::state::{AppView, BufferState}; use crate::config::storage::storage::{StoredAuthData, save_auth_data}; use crate::ui::handlers::context::DialogPurpose; use common::proto::komp_ac::auth::LoginResponse; use crate::pages::login::LoginFormState; use crate::state::pages::auth::UserRole; use canvas::DataProvider; use anyhow::{Context, Result, anyhow}; use tokio::spawn; use tokio::sync::mpsc; use tracing::{info, error}; #[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. pub async fn save( auth_state: &mut AuthState, login_state: &mut LoginFormState, auth_client: &mut AuthClient, app_state: &mut AppState, ) -> Result { let identifier = login_state.username().to_string(); let password = login_state.password().to_string(); // --- Client-side validation --- if identifier.trim().is_empty() { let error_message = "Username/Email cannot be empty.".to_string(); app_state.show_dialog( "Login Failed", &error_message, vec!["OK".to_string()], DialogPurpose::LoginFailed, ); login_state.set_error_message(Some(error_message.clone())); return Err(anyhow!(error_message)); } // Clear previous error/dialog state before attempting login_state.set_error_message(None); app_state.hide_dialog(); // Call the gRPC login method match auth_client.login(identifier.clone(), password).await .with_context(|| format!("gRPC login attempt failed for identifier: {}", identifier)) { Ok(response) => { // Store authentication details auth_state.auth_token = Some(response.access_token.clone()); auth_state.user_id = Some(response.user_id.clone()); auth_state.role = Some(UserRole::from_str(&response.role)); auth_state.decoded_username = Some(response.username.clone()); login_state.set_has_unsaved_changes(false); login_state.set_error_message(None); let success_message = format!( "Login Successful!\n\n\ Username: {}\n\ User ID: {}\n\ Role: {}", response.username, response.user_id, response.role ); app_state.show_dialog( "Login Success", &success_message, vec!["Menu".to_string(), "Exit".to_string()], DialogPurpose::LoginSuccess, ); login_state.username_mut().clear(); login_state.password_mut().clear(); login_state.set_current_cursor_pos(0); Ok("Login successful, details shown in dialog.".to_string()) } Err(e) => { let error_message = format!("{}", e); app_state.show_dialog( "Login Failed", &error_message, vec!["OK".to_string()], DialogPurpose::LoginFailed, ); login_state.set_error_message(Some(error_message.clone())); login_state.set_has_unsaved_changes(true); login_state.username_mut().clear(); login_state.password_mut().clear(); Err(e) } } } /// Reverts the login form fields to empty and returns to the previous screen (Intro). pub async fn revert( login_state: &mut LoginFormState, app_state: &mut AppState, ) -> String { // Clear the underlying state login_state.clear(); // Also clear values inside the editor’s data provider { let dp = login_state.editor.data_provider_mut(); dp.set_field_value(0, "".to_string()); dp.set_field_value(1, "".to_string()); dp.set_current_field(0); dp.set_current_cursor_pos(0); dp.set_has_unsaved_changes(false); } app_state.hide_dialog(); "Login reverted".to_string() } /// Clears login form and navigates back to main menu. pub async fn back_to_main( login_state: &mut LoginFormState, app_state: &mut AppState, buffer_state: &mut BufferState, ) -> String { login_state.clear(); app_state.hide_dialog(); buffer_state.close_active_buffer(); buffer_state.update_history(AppView::Intro); "Returned to main menu".to_string() } /// Validates input, shows loading, and spawns the login task. pub fn initiate_login( login_state: &mut LoginFormState, app_state: &mut AppState, mut auth_client: AuthClient, sender: mpsc::Sender, ) -> String { login_state.sync_from_editor(); let username = login_state.username().to_string(); let password = login_state.password().to_string(); if username.trim().is_empty() { app_state.show_dialog( "Login Failed", "Username/Email cannot be empty.", vec!["OK".to_string()], DialogPurpose::LoginFailed, ); "Username cannot be empty.".to_string() } else { app_state.show_loading_dialog("Logging In", "Please wait..."); spawn(async move { let login_outcome = match auth_client.login(username.clone(), password).await .with_context(|| format!("Spawned login task failed for identifier: {}", username)) { Ok(response) => LoginResult::Success(response), Err(e) => LoginResult::Failure(format!("{}", e)), }; if let Err(e) = sender.send(login_outcome).await { error!("Failed to send login result: {}", e); } }); "Login initiated.".to_string() } } /// Handles the result received from the login task. /// Returns true if a redraw is needed. pub fn handle_login_result( result: LoginResult, app_state: &mut AppState, auth_state: &mut AuthState, login_state: &mut LoginFormState, ) -> bool { match result { LoginResult::Success(response) => { auth_state.auth_token = Some(response.access_token.clone()); auth_state.user_id = Some(response.user_id.clone()); auth_state.role = Some(UserRole::from_str(&response.role)); auth_state.decoded_username = Some(response.username.clone()); let data_to_store = StoredAuthData { access_token: response.access_token.clone(), user_id: response.user_id.clone(), role: response.role.clone(), username: response.username.clone(), }; if let Err(e) = save_auth_data(&data_to_store) { error!("Failed to save auth data to file: {}", e); } let success_message = format!( "Login Successful!\n\nUsername: {}\nUser ID: {}\nRole: {}", response.username, response.user_id, response.role ); app_state.update_dialog_content( &success_message, vec!["Menu".to_string(), "Exit".to_string()], DialogPurpose::LoginSuccess, ); info!(message = %success_message, "Login successful"); } LoginResult::Failure(err_msg) | LoginResult::ConnectionError(err_msg) => { app_state.update_dialog_content( &err_msg, vec!["OK".to_string()], DialogPurpose::LoginFailed, ); login_state.set_error_message(Some(err_msg.clone())); error!(error = %err_msg, "Login failed/connection error"); } } login_state.username_mut().clear(); login_state.password_mut().clear(); login_state.set_has_unsaved_changes(false); login_state.set_current_cursor_pos(0); true } pub async fn handle_action(action: &str) -> Result { match action { "previous_entry" => Ok("Previous entry not implemented".into()), "next_entry" => Ok("Next entry not implemented".into()), _ => Err(anyhow!("Unknown login action: {}", action)), } }