241 lines
8.6 KiB
Rust
241 lines
8.6 KiB
Rust
// src/tui/functions/common/login.rs
|
|
|
|
use crate::services::auth::AuthClient;
|
|
use crate::state::pages::auth::AuthState;
|
|
use crate::state::pages::auth::LoginState;
|
|
use crate::state::app::state::AppState;
|
|
use crate::state::app::buffer::{AppView, BufferState};
|
|
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
|
use crate::state::pages::canvas_state::CanvasState;
|
|
use crate::ui::handlers::context::DialogPurpose;
|
|
use common::proto::komp_ac::auth::LoginResponse;
|
|
use anyhow::{Context, Result};
|
|
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 LoginState,
|
|
auth_client: &mut AuthClient,
|
|
app_state: &mut AppState,
|
|
) -> Result<String> {
|
|
let identifier = login_state.username.clone();
|
|
let password = login_state.password.clone();
|
|
|
|
// --- Client-side validation ---
|
|
// Prevent login attempt if the identifier field is empty or whitespace.
|
|
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.error_message = Some(error_message.clone());
|
|
return Err(anyhow::anyhow!(error_message));
|
|
}
|
|
|
|
// Clear previous error/dialog state before attempting
|
|
login_state.error_message = None;
|
|
app_state.hide_dialog(); // Hide any previous 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 using correct field names
|
|
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());
|
|
login_state.set_has_unsaved_changes(false);
|
|
login_state.error_message = None;
|
|
|
|
// Format the success message using response data
|
|
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.password.clear();
|
|
login_state.username.clear();
|
|
login_state.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.error_message = Some(error_message.clone());
|
|
login_state.set_has_unsaved_changes(true);
|
|
login_state.username.clear();
|
|
login_state.password.clear();
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reverts the login form fields to empty and returns to the previous screen (Intro).
|
|
pub async fn revert(
|
|
login_state: &mut LoginState,
|
|
_app_state: &mut AppState, // Keep signature consistent if needed elsewhere
|
|
) -> String {
|
|
// Clear the input fields
|
|
login_state.username.clear();
|
|
login_state.password.clear();
|
|
login_state.error_message = None;
|
|
login_state.set_has_unsaved_changes(false);
|
|
login_state.login_request_pending = false; // Ensure flag is reset on revert
|
|
|
|
"Login reverted".to_string()
|
|
}
|
|
|
|
pub async fn back_to_main(
|
|
login_state: &mut LoginState,
|
|
app_state: &mut AppState,
|
|
buffer_state: &mut BufferState,
|
|
) -> String {
|
|
// Clear the input fields
|
|
login_state.username.clear();
|
|
login_state.password.clear();
|
|
login_state.error_message = None;
|
|
login_state.set_has_unsaved_changes(false);
|
|
login_state.login_request_pending = false; // Ensure flag is reset
|
|
|
|
// Ensure dialog is hidden if revert is called
|
|
app_state.hide_dialog();
|
|
|
|
// Navigation logic
|
|
buffer_state.close_active_buffer();
|
|
buffer_state.update_history(AppView::Intro);
|
|
|
|
// Reset focus state
|
|
app_state.ui.focus_outside_canvas = false;
|
|
app_state.focused_button_index= 0;
|
|
|
|
"Returned to main menu".to_string()
|
|
}
|
|
|
|
/// Validates input, shows loading, and spawns the login task.
|
|
pub fn initiate_login(
|
|
login_state: &LoginState,
|
|
app_state: &mut AppState,
|
|
mut auth_client: AuthClient,
|
|
sender: mpsc::Sender<LoginResult>,
|
|
) -> String {
|
|
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,
|
|
);
|
|
"Username cannot be empty.".to_string()
|
|
} else {
|
|
// 2. Show Loading Dialog
|
|
app_state.show_loading_dialog("Logging In", "Please wait...");
|
|
|
|
// 3. Spawn the login task
|
|
spawn(async move {
|
|
// Use the passed-in (and moved) auth_client directly
|
|
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)),
|
|
};
|
|
// Send result back to the main UI thread
|
|
if let Err(e) = sender.send(login_outcome).await {
|
|
error!("Failed to send login result: {}", e);
|
|
}
|
|
});
|
|
|
|
// 4. Return immediately
|
|
"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 LoginState,
|
|
) -> 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(response.role.clone());
|
|
auth_state.decoded_username = Some(response.username.clone());
|
|
|
|
// --- NEW: Save auth data to file ---
|
|
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);
|
|
// Continue anyway - user is still logged in for this session
|
|
}
|
|
// --- END NEW ---
|
|
|
|
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.error_message = Some(err_msg.clone());
|
|
error!(error = %err_msg, "Login failed/connection error");
|
|
}
|
|
}
|
|
login_state.username.clear();
|
|
login_state.password.clear();
|
|
login_state.set_has_unsaved_changes(false);
|
|
login_state.current_cursor_pos = 0;
|
|
true // Request redraw as dialog content changed
|
|
}
|