Compare commits

..

2 Commits

Author SHA1 Message Date
filipriec
d8f9372bbd killing buffers 2025-05-25 22:02:18 +02:00
filipriec
6e1997fd9d storage in the system is now storing log in details properly well 2025-05-25 21:33:24 +02:00
12 changed files with 246 additions and 28 deletions

1
Cargo.lock generated
View File

@@ -409,6 +409,7 @@ dependencies = [
"prost", "prost",
"ratatui", "ratatui",
"serde", "serde",
"serde_json",
"time", "time",
"tokio", "tokio",
"toml", "toml",

View File

@@ -16,6 +16,7 @@ lazy_static = "1.5.0"
prost = "0.13.5" prost = "0.13.5"
ratatui = { version = "0.29.0", features = ["crossterm"] } ratatui = { version = "0.29.0", features = ["crossterm"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
time = "0.3.41" time = "0.3.41"
tokio = { version = "1.44.2", features = ["full", "macros"] } tokio = { version = "1.44.2", features = ["full", "macros"] }
toml = "0.8.20" toml = "0.8.20"

View File

@@ -4,6 +4,7 @@
enter_command_mode = [":", "ctrl+;"] enter_command_mode = [":", "ctrl+;"]
next_buffer = ["ctrl+l"] next_buffer = ["ctrl+l"]
previous_buffer = ["ctrl+h"] previous_buffer = ["ctrl+h"]
close_buffer = ["ctrl+k"]
[keybindings.general] [keybindings.general]
move_up = ["k", "Up"] move_up = ["k", "Up"]

View File

@@ -2,3 +2,4 @@
pub mod binds; pub mod binds;
pub mod colors; pub mod colors;
pub mod storage;

View File

@@ -0,0 +1,4 @@
// src/config/storage.rs
pub mod storage;
pub use storage::*;

View File

@@ -0,0 +1,101 @@
// src/config/storage/storage.rs
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use anyhow::{Context, Result};
use tracing::{error, info};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
pub const APP_NAME: &str = "multieko2_client";
pub const TOKEN_FILE_NAME: &str = "auth.token";
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StoredAuthData {
pub access_token: String,
pub user_id: String,
pub role: String,
pub username: String,
}
pub fn get_token_storage_path() -> Result<PathBuf> {
let state_dir = dirs::state_dir()
.or_else(|| dirs::home_dir().map(|home| home.join(".local").join("state")))
.ok_or_else(|| anyhow::anyhow!("Could not determine state directory"))?;
let app_state_dir = state_dir.join(APP_NAME);
fs::create_dir_all(&app_state_dir)
.with_context(|| format!("Failed to create app state directory at {:?}", app_state_dir))?;
Ok(app_state_dir.join(TOKEN_FILE_NAME))
}
pub fn save_auth_data(data: &StoredAuthData) -> Result<()> {
let path = get_token_storage_path()?;
let json_data = serde_json::to_string(data)
.context("Failed to serialize auth data")?;
let mut file = File::create(&path)
.with_context(|| format!("Failed to create token file at {:?}", path))?;
file.write_all(json_data.as_bytes())
.context("Failed to write token data to file")?;
// Set file permissions to 600 (owner read/write only) on Unix
#[cfg(unix)]
{
file.set_permissions(std::fs::Permissions::from_mode(0o600))
.context("Failed to set token file permissions")?;
}
info!("Auth data saved to {:?}", path);
Ok(())
}
pub fn load_auth_data() -> Result<Option<StoredAuthData>> {
let path = get_token_storage_path()?;
if !path.exists() {
info!("Token file not found at {:?}", path);
return Ok(None);
}
let json_data = fs::read_to_string(&path)
.with_context(|| format!("Failed to read token file at {:?}", path))?;
if json_data.trim().is_empty() {
info!("Token file is empty at {:?}", path);
return Ok(None);
}
match serde_json::from_str::<StoredAuthData>(&json_data) {
Ok(data) => {
info!("Auth data loaded from {:?}", path);
Ok(Some(data))
}
Err(e) => {
error!("Failed to deserialize token data from {:?}: {}. Deleting corrupt file.", path, e);
if let Err(del_e) = fs::remove_file(&path) {
error!("Failed to delete corrupt token file: {}", del_e);
}
Ok(None)
}
}
}
pub fn delete_auth_data() -> Result<()> {
let path = get_token_storage_path()?;
if path.exists() {
fs::remove_file(&path)
.with_context(|| format!("Failed to delete token file at {:?}", path))?;
info!("Token file deleted from {:?}", path);
} else {
info!("Token file not found for deletion at {:?}", path);
}
Ok(())
}

View File

@@ -161,6 +161,7 @@ impl EventHandler {
return Ok(EventOutcome::Ok(message)); return Ok(EventOutcome::Ok(message));
} }
if !matches!(current_mode, AppMode::Edit | AppMode::Command) { if !matches!(current_mode, AppMode::Edit | AppMode::Command) {
if let Some(action) = config.get_action_for_key_in_mode( if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.global, key_code, modifiers &config.keybindings.global, key_code, modifiers
@@ -176,6 +177,10 @@ impl EventHandler {
return Ok(EventOutcome::Ok("Switched to previous buffer".to_string())); return Ok(EventOutcome::Ok("Switched to previous buffer".to_string()));
} }
} }
"close_buffer" => {
let message = buffer_state.close_buffer_with_intro_fallback();
return Ok(EventOutcome::Ok(message));
}
_ => {} _ => {}
} }
} }

View File

@@ -1,6 +1,5 @@
// src/state/app/buffer.rs // src/state/app/buffer.rs
/// Represents the distinct views or "buffers" the user can navigate.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppView { pub enum AppView {
Intro, Intro,
@@ -14,7 +13,6 @@ pub enum AppView {
} }
impl AppView { impl AppView {
/// Returns the display name for the view.
pub fn display_name(&self) -> &str { pub fn display_name(&self) -> &str {
match self { match self {
AppView::Intro => "Intro", AppView::Intro => "Intro",
@@ -29,7 +27,6 @@ impl AppView {
} }
} }
/// Holds the state related to buffer management (navigation history).
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BufferState { pub struct BufferState {
pub history: Vec<AppView>, pub history: Vec<AppView>,
@@ -39,23 +36,17 @@ pub struct BufferState {
impl Default for BufferState { impl Default for BufferState {
fn default() -> Self { fn default() -> Self {
Self { Self {
history: vec![AppView::Intro], // Start with Intro view history: vec![AppView::Intro],
active_index: 0, active_index: 0,
} }
} }
} }
impl BufferState { impl BufferState {
/// Updates the buffer history and active index.
/// If the view already exists, it sets it as active.
/// Otherwise, it adds the new view and makes it active.
pub fn update_history(&mut self, view: AppView) { pub fn update_history(&mut self, view: AppView) {
let existing_pos = self.history.iter().position(|v| v == &view); let existing_pos = self.history.iter().position(|v| v == &view);
match existing_pos { match existing_pos {
Some(pos) => { Some(pos) => self.active_index = pos,
self.active_index = pos;
}
None => { None => {
self.history.push(view.clone()); self.history.push(view.clone());
self.active_index = self.history.len() - 1; self.active_index = self.history.len() - 1;
@@ -63,34 +54,51 @@ impl BufferState {
} }
} }
/// Gets the currently active view.
pub fn get_active_view(&self) -> Option<&AppView> { pub fn get_active_view(&self) -> Option<&AppView> {
self.history.get(self.active_index) self.history.get(self.active_index)
} }
/// Removes the currently active buffer from the history, unless it's the Intro buffer.
/// Sets the new active buffer to the one preceding the closed one.
/// # Returns
/// * `true` if a non-Intro buffer was closed.
/// * `false` if the active buffer was Intro or only Intro remained.
pub fn close_active_buffer(&mut self) -> bool { pub fn close_active_buffer(&mut self) -> bool {
let current_index = self.active_index; if self.history.is_empty() {
self.history.push(AppView::Intro);
self.active_index = 0;
return false;
}
// Rule 1: Cannot close Intro buffer. let current_index = self.active_index;
if matches!(self.history.get(current_index), Some(AppView::Intro)) { if matches!(self.history.get(current_index), Some(AppView::Intro)) {
return false; return false;
} }
// Rule 2: Cannot close if only Intro would remain (or already remains).
// This check implicitly covers the case where len <= 1.
if self.history.len() <= 1 {
return false;
}
self.history.remove(current_index); self.history.remove(current_index);
self.active_index = current_index.saturating_sub(1).min(self.history.len() - 1); if self.history.is_empty() {
self.history.push(AppView::Intro);
self.active_index = 0;
} else if self.active_index >= self.history.len() {
self.active_index = self.history.len() - 1;
}
true true
} }
}
pub fn close_buffer_with_intro_fallback(&mut self) -> String {
let current_view_cloned = self.get_active_view().cloned();
if let Some(AppView::Intro) = current_view_cloned {
return "Cannot close intro buffer".to_string();
}
let closed_name = current_view_cloned
.as_ref()
.map(|v| v.display_name().to_string())
.unwrap_or_else(|| "Unknown".to_string());
if self.close_active_buffer() {
if self.history.len() == 1 && matches!(self.history.get(0), Some(AppView::Intro)) {
format!("Closed '{}' - returned to intro", closed_name)
} else {
format!("Closed '{}'", closed_name)
}
} else {
format!("Cannot close buffer: {}", closed_name)
}
}
}

View File

@@ -2,5 +2,6 @@
pub mod form; pub mod form;
pub mod login; pub mod login;
pub mod logout;
pub mod register; pub mod register;
pub mod add_table; pub mod add_table;

View File

@@ -5,6 +5,7 @@ use crate::state::pages::auth::AuthState;
use crate::state::pages::auth::LoginState; use crate::state::pages::auth::LoginState;
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::state::app::buffer::{AppView, BufferState}; 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::state::pages::canvas_state::CanvasState;
use crate::ui::handlers::context::DialogPurpose; use crate::ui::handlers::context::DialogPurpose;
use common::proto::multieko2::auth::LoginResponse; use common::proto::multieko2::auth::LoginResponse;
@@ -200,6 +201,20 @@ pub fn handle_login_result(
auth_state.role = Some(response.role.clone()); auth_state.role = Some(response.role.clone());
auth_state.decoded_username = Some(response.username.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!( let success_message = format!(
"Login Successful!\n\nUsername: {}\nUser ID: {}\nRole: {}", "Login Successful!\n\nUsername: {}\nUser ID: {}\nRole: {}",
response.username, response.user_id, response.role response.username, response.user_id, response.role

View File

@@ -0,0 +1,47 @@
// src/tui/functions/common/logout.rs
use crate::config::storage::delete_auth_data;
use crate::state::pages::auth::AuthState;
use crate::state::app::state::AppState;
use crate::state::app::buffer::{AppView, BufferState};
use crate::ui::handlers::context::DialogPurpose;
use tracing::{error, info};
pub fn logout(
auth_state: &mut AuthState,
app_state: &mut AppState,
buffer_state: &mut BufferState,
) -> String {
// Clear auth state in memory
auth_state.auth_token = None;
auth_state.user_id = None;
auth_state.role = None;
auth_state.decoded_username = None;
// Delete stored auth data
if let Err(e) = delete_auth_data() {
error!("Failed to delete stored auth data: {}", e);
// Continue anyway - user is logged out in memory
}
// Navigate to intro screen
buffer_state.history = vec![AppView::Intro];
buffer_state.active_index = 0;
// Reset UI state
app_state.ui.focus_outside_canvas = false;
app_state.focused_button_index = 0;
// Hide any open dialogs
app_state.hide_dialog();
// Show logout confirmation dialog
app_state.show_dialog(
"Logged Out",
"You have been successfully logged out.",
vec!["OK".to_string()],
DialogPurpose::LoginSuccess, // Reuse or create a new purpose
);
info!("User logged out successfully.");
"Logged out successfully".to_string()
}

View File

@@ -4,6 +4,7 @@ use crate::config::binds::config::Config;
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::services::ui_service::UiService; use crate::services::ui_service::UiService;
use crate::config::storage::storage::load_auth_data;
use crate::modes::common::commands::CommandHandler; use crate::modes::common::commands::CommandHandler;
use crate::modes::handlers::event::{EventHandler, EventOutcome}; use crate::modes::handlers::event::{EventHandler, EventOutcome};
use crate::modes::handlers::mode_manager::{AppMode, ModeManager}; use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
@@ -68,6 +69,28 @@ pub async fn run_ui() -> Result<()> {
let mut buffer_state = BufferState::default(); let mut buffer_state = BufferState::default();
let mut app_state = AppState::new().context("Failed to create initial app state")?; let mut app_state = AppState::new().context("Failed to create initial app state")?;
// --- DATA: Load auth data from file at startup ---
let mut auto_logged_in = false;
match load_auth_data() {
Ok(Some(stored_data)) => {
// TODO: Optionally validate token with server here
// For now, assume valid if successfully loaded
auth_state.auth_token = Some(stored_data.access_token);
auth_state.user_id = Some(stored_data.user_id);
auth_state.role = Some(stored_data.role);
auth_state.decoded_username = Some(stored_data.username);
auto_logged_in = true;
info!("Auth data loaded from file. User is auto-logged in.");
}
Ok(None) => {
info!("No stored auth data found. User will see intro/login.");
}
Err(e) => {
error!("Failed to load auth data: {}", e);
}
}
// --- END DATA ---
// Initialize app state with profile tree and table structure // Initialize app state with profile tree and table structure
let column_names = let column_names =
UiService::initialize_app_state(&mut grpc_client, &mut app_state) UiService::initialize_app_state(&mut grpc_client, &mut app_state)
@@ -78,6 +101,16 @@ pub async fn run_ui() -> Result<()> {
UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?; UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?;
form_state.reset_to_empty(); form_state.reset_to_empty();
// --- DATA2: Adjust initial view based on auth status ---
if auto_logged_in {
// User is auto-logged in, go to main app view
buffer_state.history = vec![AppView::Form("Adresar".to_string())];
buffer_state.active_index = 0;
info!("Initial view set to Form due to auto-login.");
}
// If not auto-logged in, BufferState default (Intro) will be used
// --- END DATA2 ---
// --- FPS Calculation State --- // --- FPS Calculation State ---
let mut last_frame_time = Instant::now(); let mut last_frame_time = Instant::now();
let mut current_fps = 0.0; let mut current_fps = 0.0;