login page using canvas for forms

This commit is contained in:
filipriec
2025-08-28 21:07:23 +02:00
parent 6e221ef8c1
commit 19a9bab8c2
8 changed files with 195 additions and 101 deletions

View File

@@ -1,4 +1,4 @@
// src/tui/functions/common/login.rs
// src/pages/login/logic.rs
use crate::services::auth::AuthClient;
use crate::state::pages::auth::AuthState;
@@ -7,12 +7,11 @@ 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::LoginState;
use anyhow::{Context, Result};
use crate::pages::login::LoginFormState;
use anyhow::{Context, Result, anyhow};
use tokio::spawn;
use tokio::sync::mpsc;
use tracing::{info, error};
use anyhow::anyhow;
#[derive(Debug)]
pub enum LoginResult {
@@ -25,15 +24,14 @@ pub enum LoginResult {
/// Updates AuthState and AppState on success or failure.
pub async fn save(
auth_state: &mut AuthState,
login_state: &mut LoginState,
login_state: &mut LoginFormState,
auth_client: &mut AuthClient,
app_state: &mut AppState,
) -> Result<String> {
let identifier = login_state.username.clone();
let password = login_state.password.clone();
let identifier = login_state.username().to_string();
let password = login_state.password().to_string();
// --- 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(
@@ -42,33 +40,33 @@ pub async fn save(
vec!["OK".to_string()],
DialogPurpose::LoginFailed,
);
login_state.error_message = Some(error_message.clone());
return Err(anyhow::anyhow!(error_message));
login_state.set_error_message(Some(error_message.clone()));
return Err(anyhow!(error_message));
}
// Clear previous error/dialog state before attempting
login_state.error_message = None;
app_state.hide_dialog(); // Hide any previous dialog
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 using correct field names
// 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(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
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: {}",
Username: {}\n\
User ID: {}\n\
Role: {}",
response.username,
response.user_id,
response.role
@@ -80,9 +78,11 @@ pub async fn save(
vec!["Menu".to_string(), "Exit".to_string()],
DialogPurpose::LoginSuccess,
);
login_state.password.clear();
login_state.username.clear();
login_state.current_cursor_pos = 0;
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) => {
@@ -93,10 +93,10 @@ pub async fn save(
vec!["OK".to_string()],
DialogPurpose::LoginFailed,
);
login_state.error_message = Some(error_message.clone());
login_state.set_error_message(Some(error_message.clone()));
login_state.set_has_unsaved_changes(true);
login_state.username.clear();
login_state.password.clear();
login_state.username_mut().clear();
login_state.password_mut().clear();
Err(e)
}
}
@@ -104,56 +104,42 @@ pub async fn save(
/// 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
login_state: &mut LoginFormState,
app_state: &mut AppState,
) -> 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_state.clear();
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 LoginState,
login_state: &mut LoginFormState,
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
login_state.clear();
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;
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,
login_state: &LoginFormState,
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();
let username = login_state.username().to_string();
let password = login_state.password().to_string();
// 1. Client-side validation
if username.trim().is_empty() {
app_state.show_dialog(
"Login Failed",
@@ -163,25 +149,20 @@ pub fn initiate_login(
);
"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
{
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);
}
});
// 4. Return immediately
"Login initiated.".to_string()
}
}
@@ -192,7 +173,7 @@ pub fn handle_login_result(
result: LoginResult,
app_state: &mut AppState,
auth_state: &mut AuthState,
login_state: &mut LoginState,
login_state: &mut LoginFormState,
) -> bool {
match result {
LoginResult::Success(response) => {
@@ -201,19 +182,15 @@ pub fn handle_login_result(
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: {}",
@@ -227,26 +204,28 @@ pub fn handle_login_result(
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());
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.clear();
login_state.password.clear();
login_state.username_mut().clear();
login_state.password_mut().clear();
login_state.set_has_unsaved_changes(false);
login_state.current_cursor_pos = 0;
true // Request redraw as dialog content changed
login_state.set_current_cursor_pos(0);
true
}
pub async fn handle_action(action: &str,) -> Result<String> {
pub async fn handle_action(action: &str) -> Result<String> {
match action {
"previous_entry" => {
Ok("Previous entry at tui/functions/login.rs not implemented".into())
}
"next_entry" => {
Ok("Next entry at tui/functions/login.rs not implemented".into())
}
_ => Err(anyhow!("Unknown login action: {}", action))
"previous_entry" => Ok("Previous entry not implemented".into()),
"next_entry" => Ok("Next entry not implemented".into()),
_ => Err(anyhow!("Unknown login action: {}", action)),
}
}