Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd5f685a6 | ||
|
|
c1c4394f94 | ||
|
|
16d9fcdadc | ||
|
|
5b1db01fe6 | ||
|
|
e856e9d6c7 | ||
|
|
ad2c783870 | ||
|
|
5e101bef14 | ||
|
|
149949ad99 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -436,6 +436,7 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"unicode-segmentation",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ tokio = { version = "1.43.0", features = ["full", "macros"] }
|
|||||||
toml = "0.8.20"
|
toml = "0.8.20"
|
||||||
tonic = "0.12.3"
|
tonic = "0.12.3"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
unicode-width = "0.2.0"
|
unicode-width = "0.2.0"
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ exit_edit_mode = ["esc","ctrl+e"]
|
|||||||
delete_char_forward = ["delete"]
|
delete_char_forward = ["delete"]
|
||||||
delete_char_backward = ["backspace"]
|
delete_char_backward = ["backspace"]
|
||||||
next_field = ["enter"]
|
next_field = ["enter"]
|
||||||
prev_field = ["backtab"]
|
prev_field = ["shift+enter"]
|
||||||
move_left = ["left"]
|
move_left = ["left"]
|
||||||
move_right = ["right"]
|
move_right = ["right"]
|
||||||
suggestion_down = ["shift+tab"]
|
suggestion_down = ["ctrl+n", "tab"]
|
||||||
suggestion_up = ["tab"]
|
suggestion_up = ["ctrl+p", "shift+tab"]
|
||||||
select_suggestion = ["enter"]
|
select_suggestion = ["enter"]
|
||||||
exit_suggestion_mode = ["esc"]
|
exit_suggestion_mode = ["esc"]
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::{
|
|||||||
components::common::{dialog, autocomplete},
|
components::common::{dialog, autocomplete},
|
||||||
state::state::AppState,
|
state::state::AppState,
|
||||||
state::canvas_state::CanvasState,
|
state::canvas_state::CanvasState,
|
||||||
|
modes::handlers::mode_manager::AppMode,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
||||||
@@ -41,6 +42,7 @@ pub fn render_register(
|
|||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(7), // Form (5 fields + padding)
|
Constraint::Length(7), // Form (5 fields + padding)
|
||||||
|
Constraint::Length(1), // Help text line
|
||||||
Constraint::Length(1), // Error message
|
Constraint::Length(1), // Error message
|
||||||
Constraint::Length(3), // Buttons
|
Constraint::Length(3), // Buttons
|
||||||
])
|
])
|
||||||
@@ -53,25 +55,31 @@ pub fn render_register(
|
|||||||
state, // The state object (RegisterState)
|
state, // The state object (RegisterState)
|
||||||
&[ // Field labels
|
&[ // Field labels
|
||||||
"Username",
|
"Username",
|
||||||
"Email (Optional)",
|
"Email*",
|
||||||
"Password (Optional)",
|
"Password*",
|
||||||
"Confirm Password",
|
"Confirm Password",
|
||||||
"Role (Optional)",
|
"Role* (Tab)",
|
||||||
],
|
],
|
||||||
&state.current_field(), // Pass current field index
|
&state.current_field(), // Pass current field index
|
||||||
&state.inputs().iter().map(|s| *s).collect::<Vec<&String>>(), // Pass inputs directly
|
&state.inputs().iter().map(|s| *s).collect::<Vec<&String>>(), // Pass inputs directly
|
||||||
theme,
|
theme,
|
||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
// No need to pass suggestion state here, render_canvas uses the trait
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- HELP TEXT ---
|
||||||
|
let help_text = Paragraph::new("* are optional fields")
|
||||||
|
.style(Style::default().fg(theme.fg))
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
f.render_widget(help_text, chunks[1]);
|
||||||
|
|
||||||
|
|
||||||
// --- ERROR MESSAGE ---
|
// --- ERROR MESSAGE ---
|
||||||
if let Some(err) = &state.error_message {
|
if let Some(err) = &state.error_message {
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(err.as_str())
|
Paragraph::new(err.as_str())
|
||||||
.style(Style::default().fg(Color::Red))
|
.style(Style::default().fg(Color::Red))
|
||||||
.alignment(Alignment::Center),
|
.alignment(Alignment::Center),
|
||||||
chunks[1],
|
chunks[2],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +87,7 @@ pub fn render_register(
|
|||||||
let button_chunks = Layout::default()
|
let button_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
.split(chunks[2]);
|
.split(chunks[3]);
|
||||||
|
|
||||||
// Register Button
|
// Register Button
|
||||||
let register_button_index = 0;
|
let register_button_index = 0;
|
||||||
@@ -136,11 +144,13 @@ pub fn render_register(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- Render Autocomplete Dropdown (Draw AFTER buttons) ---
|
// --- Render Autocomplete Dropdown (Draw AFTER buttons) ---
|
||||||
if let Some(suggestions) = state.get_suggestions() {
|
if app_state.current_mode == AppMode::Edit {
|
||||||
let selected = state.get_selected_suggestion_index();
|
if let Some(suggestions) = state.get_suggestions() {
|
||||||
if !suggestions.is_empty() {
|
let selected = state.get_selected_suggestion_index();
|
||||||
if let Some(input_rect) = active_field_rect {
|
if !suggestions.is_empty() {
|
||||||
autocomplete::render_autocomplete_dropdown(f, input_rect, f.size(), theme, suggestions, selected);
|
if let Some(input_rect) = active_field_rect {
|
||||||
|
autocomplete::render_autocomplete_dropdown(f, input_rect, f.size(), theme, suggestions, selected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,11 @@ pub fn render_autocomplete_dropdown(
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, s)| {
|
.map(|(i, s)| {
|
||||||
let is_selected = selected_index == Some(i);
|
let is_selected = selected_index == Some(i);
|
||||||
ListItem::new(s.as_str()).style(if is_selected {
|
let s_width = s.width() as u16;
|
||||||
// Style for selected item (highlight background)
|
let padding_needed = dropdown_width.saturating_sub(s_width);
|
||||||
|
let padded_s = format!("{}{}", s, " ".repeat(padding_needed as usize));
|
||||||
|
|
||||||
|
ListItem::new(padded_s).style(if is_selected {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(theme.bg) // Text color on highlight
|
.fg(theme.bg) // Text color on highlight
|
||||||
.bg(theme.highlight) // Highlight background
|
.bg(theme.highlight) // Highlight background
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// src/components/common/dialog.rs
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||||
prelude::Alignment,
|
prelude::Alignment,
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, BorderType, Borders, Paragraph, Clear}, // Added Clear
|
widgets::{Block, BorderType, Borders, Paragraph, Clear},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation; // For grapheme clusters
|
||||||
|
use unicode_width::UnicodeWidthStr; // For accurate width calculation
|
||||||
|
|
||||||
pub fn render_dialog(
|
pub fn render_dialog(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -18,15 +19,16 @@ pub fn render_dialog(
|
|||||||
dialog_buttons: &[String],
|
dialog_buttons: &[String],
|
||||||
dialog_active_button_index: usize,
|
dialog_active_button_index: usize,
|
||||||
) {
|
) {
|
||||||
|
// Calculate required height based on the actual number of lines in the message
|
||||||
let message_lines: Vec<_> = dialog_message.lines().collect();
|
let message_lines: Vec<_> = dialog_message.lines().collect();
|
||||||
let message_height = message_lines.len() as u16;
|
let message_height = message_lines.len() as u16;
|
||||||
let button_row_height = if dialog_buttons.is_empty() { 0 } else { 3 };
|
let button_row_height = if dialog_buttons.is_empty() { 0 } else { 3 };
|
||||||
let vertical_padding = 2; // Block borders (top/bottom)
|
let vertical_padding = 2; // Block borders (top/bottom)
|
||||||
let inner_vertical_margin = 2; // Margin inside block (top/bottom)
|
let inner_vertical_margin = 2; // Margin inside block (top/bottom)
|
||||||
|
|
||||||
|
// Calculate required height based on actual message lines
|
||||||
let required_inner_height =
|
let required_inner_height =
|
||||||
message_height + button_row_height + inner_vertical_margin;
|
message_height + button_row_height + inner_vertical_margin;
|
||||||
// Add block border height
|
|
||||||
let required_total_height = required_inner_height + vertical_padding;
|
let required_total_height = required_inner_height + vertical_padding;
|
||||||
|
|
||||||
// Use a fixed percentage width, clamped to min/max
|
// Use a fixed percentage width, clamped to min/max
|
||||||
@@ -61,10 +63,10 @@ pub fn render_dialog(
|
|||||||
vertical: 1, // Top/Bottom padding inside border
|
vertical: 1, // Top/Bottom padding inside border
|
||||||
});
|
});
|
||||||
|
|
||||||
// Layout for Message and Buttons
|
// Layout for Message and Buttons based on actual message height
|
||||||
let mut constraints = vec![
|
let mut constraints = vec![
|
||||||
// Allocate space for message, ensuring at least 1 line height
|
// Allocate space for message, ensuring at least 1 line height
|
||||||
Constraint::Min(message_height.max(1)),
|
Constraint::Length(message_height.max(1)), // Use actual calculated height
|
||||||
];
|
];
|
||||||
if button_row_height > 0 {
|
if button_row_height > 0 {
|
||||||
constraints.push(Constraint::Length(button_row_height));
|
constraints.push(Constraint::Length(button_row_height));
|
||||||
@@ -76,15 +78,39 @@ pub fn render_dialog(
|
|||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// Render Message
|
// Render Message
|
||||||
let message_text = Text::from(
|
let available_width = inner_area.width as usize;
|
||||||
|
let ellipsis = "...";
|
||||||
|
let ellipsis_width = UnicodeWidthStr::width(ellipsis);
|
||||||
|
|
||||||
|
let processed_lines: Vec<Line> =
|
||||||
message_lines
|
message_lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|l| Line::from(Span::styled(l, Style::default().fg(theme.fg))))
|
.map(|line| {
|
||||||
.collect::<Vec<_>>(),
|
let line_width = UnicodeWidthStr::width(line);
|
||||||
);
|
if line_width > available_width {
|
||||||
|
// Truncate with ellipsis
|
||||||
|
let mut truncated_len = 0;
|
||||||
|
let mut current_width = 0;
|
||||||
|
// Iterate over graphemes to handle multi-byte characters correctly
|
||||||
|
for (idx, grapheme) in line.grapheme_indices(true) {
|
||||||
|
let grapheme_width = UnicodeWidthStr::width(grapheme);
|
||||||
|
if current_width + grapheme_width > available_width.saturating_sub(ellipsis_width) {
|
||||||
|
break; // Stop before exceeding width needed for text + ellipsis
|
||||||
|
}
|
||||||
|
current_width += grapheme_width;
|
||||||
|
truncated_len = idx + grapheme.len(); // Store the byte index of the end of the last fitting grapheme
|
||||||
|
}
|
||||||
|
let truncated_line = format!("{}{}", &line[..truncated_len], ellipsis);
|
||||||
|
Line::from(Span::styled(truncated_line, Style::default().fg(theme.fg)))
|
||||||
|
} else {
|
||||||
|
// Line fits, use it as is
|
||||||
|
Line::from(Span::styled(line, Style::default().fg(theme.fg)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let message_paragraph =
|
let message_paragraph =
|
||||||
Paragraph::new(message_text).alignment(Alignment::Center);
|
Paragraph::new(Text::from(processed_lines)).alignment(Alignment::Center);
|
||||||
// Render message in the first chunk
|
// Render message in the first chunk
|
||||||
f.render_widget(message_paragraph, chunks[0]);
|
f.render_widget(message_paragraph, chunks[0]);
|
||||||
|
|
||||||
@@ -143,4 +169,3 @@ pub fn render_dialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ pub async fn execute_edit_action<S: CanvasState + Any + Send>(
|
|||||||
register_state.selected_suggestion_index = Some(if current_index >= max_index { 0 } else { current_index + 1 });
|
register_state.selected_suggestion_index = Some(if current_index >= max_index { 0 } else { current_index + 1 });
|
||||||
Ok("Suggestion changed down".to_string())
|
Ok("Suggestion changed down".to_string())
|
||||||
}
|
}
|
||||||
"suggestion_up" if register_state.show_role_suggestions => {
|
"suggestion_up" if register_state.in_suggestion_mode => {
|
||||||
let max_index = register_state.role_suggestions.len().saturating_sub(1);
|
let max_index = register_state.role_suggestions.len().saturating_sub(1);
|
||||||
let current_index = register_state.selected_suggestion_index.unwrap_or(0);
|
let current_index = register_state.selected_suggestion_index.unwrap_or(0);
|
||||||
register_state.selected_suggestion_index = Some(if current_index == 0 { max_index } else { current_index.saturating_sub(1) });
|
register_state.selected_suggestion_index = Some(if current_index == 0 { max_index } else { current_index.saturating_sub(1) });
|
||||||
@@ -336,10 +336,12 @@ pub async fn execute_edit_action<S: CanvasState + Any + Send>(
|
|||||||
register_state.in_suggestion_mode = false;
|
register_state.in_suggestion_mode = false;
|
||||||
Ok("Suggestions hidden".to_string())
|
Ok("Suggestions hidden".to_string())
|
||||||
}
|
}
|
||||||
_ => Ok("".to_string()) // Action doesn't apply in this state (e.g., suggestions not shown)
|
"suggestion_down" | "suggestion_up" | "select_suggestion" => {
|
||||||
|
Ok("Suggestion action ignored: Not in suggestion mode.".to_string())
|
||||||
|
}
|
||||||
|
_ => Ok("".to_string())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Action received but not applicable to the current field
|
|
||||||
Ok("".to_string())
|
Ok("".to_string())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::state::pages::form::FormState;
|
|||||||
use crate::functions::modes::edit::{auth_e, form_e};
|
use crate::functions::modes::edit::{auth_e, form_e};
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::state::state::AppState;
|
use crate::state::state::AppState;
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
pub async fn handle_edit_event(
|
pub async fn handle_edit_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -82,24 +82,19 @@ pub async fn handle_edit_event(
|
|||||||
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
||||||
// --- Special Handling for Tab/Shift+Tab in Role Field ---
|
// --- Special Handling for Tab/Shift+Tab in Role Field ---
|
||||||
if app_state.ui.show_register && register_state.current_field() == 4 {
|
if app_state.ui.show_register && register_state.current_field() == 4 {
|
||||||
match action {
|
if !register_state.in_suggestion_mode && key.code == KeyCode::Tab && key.modifiers == KeyModifiers::NONE {
|
||||||
"suggestion_up" | "suggestion_down" => { // Mapped to Tab/Shift+Tab
|
register_state.update_role_suggestions();
|
||||||
if !register_state.in_suggestion_mode {
|
if !register_state.role_suggestions.is_empty() {
|
||||||
register_state.update_role_suggestions();
|
register_state.in_suggestion_mode = true;
|
||||||
if !register_state.role_suggestions.is_empty() {
|
register_state.selected_suggestion_index = Some(0); // Select first suggestion
|
||||||
register_state.in_suggestion_mode = true;
|
return Ok("Suggestions shown".to_string());
|
||||||
register_state.selected_suggestion_index = Some(0);
|
} else {
|
||||||
return Ok("Suggestions shown".to_string());
|
return Ok("No suggestions available".to_string());
|
||||||
} else {
|
|
||||||
return Ok("No suggestions available".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- End Special Handling ---
|
// --- End Special Handling ---
|
||||||
|
|
||||||
return if app_state.ui.show_login {
|
return if app_state.ui.show_login {
|
||||||
auth_e::execute_edit_action(
|
auth_e::execute_edit_action(
|
||||||
action,
|
action,
|
||||||
|
|||||||
@@ -11,21 +11,28 @@ lazy_static! {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the authenticated session state
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AuthState {
|
pub struct AuthState {
|
||||||
pub return_selected: bool,
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
pub current_field: usize,
|
|
||||||
pub current_cursor_pos: usize,
|
|
||||||
pub auth_token: Option<String>,
|
pub auth_token: Option<String>,
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
pub role: Option<String>,
|
pub role: Option<String>,
|
||||||
|
pub decoded_username: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the state of the Login form UI
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LoginState {
|
||||||
|
pub username: String, // Input field for username/email
|
||||||
|
pub password: String, // Input field for password
|
||||||
|
pub error_message: Option<String>, // Error message specific to login attempt
|
||||||
|
pub current_field: usize, // 0 for username, 1 for password
|
||||||
|
pub current_cursor_pos: usize, // Cursor position within current field
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)] // Add Clone derive
|
/// Represents the state of the Registration form UI
|
||||||
|
#[derive(Default, Clone)]
|
||||||
pub struct RegisterState {
|
pub struct RegisterState {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
@@ -43,23 +50,33 @@ pub struct RegisterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AuthState {
|
impl AuthState {
|
||||||
|
/// Creates a new empty AuthState (unauthenticated)
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
auth_token: None,
|
||||||
|
user_id: None,
|
||||||
|
role: None,
|
||||||
|
decoded_username: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginState {
|
||||||
|
/// Creates a new empty LoginState
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
return_selected: false,
|
|
||||||
username: String::new(),
|
username: String::new(),
|
||||||
password: String::new(),
|
password: String::new(),
|
||||||
error_message: None,
|
error_message: None,
|
||||||
current_field: 0,
|
current_field: 0,
|
||||||
current_cursor_pos: 0,
|
current_cursor_pos: 0,
|
||||||
auth_token: None,
|
|
||||||
user_id: None,
|
|
||||||
role: None,
|
|
||||||
has_unsaved_changes: false,
|
has_unsaved_changes: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterState {
|
impl RegisterState {
|
||||||
|
/// Creates a new empty RegisterState
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
username: String::new(),
|
username: String::new(),
|
||||||
@@ -78,7 +95,7 @@ impl RegisterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates role suggestions based on current input.
|
/// Updates role suggestions based on current input
|
||||||
pub fn update_role_suggestions(&mut self) {
|
pub fn update_role_suggestions(&mut self) {
|
||||||
let current_input = self.role.to_lowercase();
|
let current_input = self.role.to_lowercase();
|
||||||
self.role_suggestions = AVAILABLE_ROLES
|
self.role_suggestions = AVAILABLE_ROLES
|
||||||
@@ -90,7 +107,7 @@ impl RegisterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasState for AuthState {
|
impl CanvasState for LoginState {
|
||||||
fn current_field(&self) -> usize {
|
fn current_field(&self) -> usize {
|
||||||
self.current_field
|
self.current_field
|
||||||
}
|
}
|
||||||
@@ -124,7 +141,7 @@ impl CanvasState for AuthState {
|
|||||||
match self.current_field {
|
match self.current_field {
|
||||||
0 => &mut self.username,
|
0 => &mut self.username,
|
||||||
1 => &mut self.password,
|
1 => &mut self.password,
|
||||||
_ => panic!("Invalid current_field index in AuthState"),
|
_ => panic!("Invalid current_field index in LoginState"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,13 +150,12 @@ impl CanvasState for AuthState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
if index < 2 { // AuthState only has 2 fields
|
if index < 2 {
|
||||||
self.current_field = index;
|
self.current_field = index;
|
||||||
// IMPORTANT: Clamp cursor position to the length of the NEW field
|
|
||||||
let len = match self.current_field {
|
let len = match self.current_field {
|
||||||
0 => self.username.len(),
|
0 => self.username.len(),
|
||||||
1 => self.password.len(),
|
1 => self.password.len(),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
self.current_cursor_pos = self.current_cursor_pos.min(len);
|
self.current_cursor_pos = self.current_cursor_pos.min(len);
|
||||||
}
|
}
|
||||||
@@ -147,26 +163,23 @@ impl CanvasState for AuthState {
|
|||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
let len = match self.current_field {
|
let len = match self.current_field {
|
||||||
0 => self.username.len(),
|
0 => self.username.len(),
|
||||||
1 => self.password.len(),
|
1 => self.password.len(),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
// Ensure stored position is always valid
|
|
||||||
self.current_cursor_pos = pos.min(len);
|
self.current_cursor_pos = pos.min(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
// Allow the generic handler to signal changes
|
|
||||||
self.has_unsaved_changes = changed;
|
self.has_unsaved_changes = changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Autocomplete Support (Not Used for AuthState) ---
|
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
fn get_suggestions(&self) -> Option<&[String]> {
|
||||||
None // AuthState doesn't provide suggestions
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||||
None // AuthState doesn't have selected suggestions
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,12 +242,12 @@ impl CanvasState for RegisterState {
|
|||||||
"Email (Optional)",
|
"Email (Optional)",
|
||||||
"Password (Optional)",
|
"Password (Optional)",
|
||||||
"Confirm Password",
|
"Confirm Password",
|
||||||
"Role (Oprional)"
|
"Role (Optional)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
if index < 5 { // RegisterState has 5 fields
|
if index < 5 {
|
||||||
self.current_field = index;
|
self.current_field = index;
|
||||||
let len = match self.current_field {
|
let len = match self.current_field {
|
||||||
0 => self.username.len(),
|
0 => self.username.len(),
|
||||||
@@ -265,7 +278,6 @@ impl CanvasState for RegisterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
fn get_suggestions(&self) -> Option<&[String]> {
|
||||||
// Only show suggestions for the role field (index 4) when requested
|
|
||||||
if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions {
|
if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions {
|
||||||
Some(&self.role_suggestions)
|
Some(&self.role_suggestions)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -28,25 +28,19 @@ pub async fn save(
|
|||||||
auth_state.auth_token = Some(response.access_token.clone());
|
auth_state.auth_token = Some(response.access_token.clone());
|
||||||
auth_state.user_id = Some(response.user_id.clone());
|
auth_state.user_id = Some(response.user_id.clone());
|
||||||
auth_state.role = Some(response.role.clone());
|
auth_state.role = Some(response.role.clone());
|
||||||
|
auth_state.decoded_username = Some(response.username.clone());
|
||||||
auth_state.set_has_unsaved_changes(false);
|
auth_state.set_has_unsaved_changes(false);
|
||||||
|
|
||||||
let success_message = format!(
|
let success_message = format!(
|
||||||
"Login Successful!\n\n\
|
"Login Successful!\n\n\
|
||||||
Access Token: {}\n\
|
Username: {}\n\
|
||||||
Token Type: {}\n\
|
|
||||||
Expires In: {}\n\
|
|
||||||
User ID: {}\n\
|
User ID: {}\n\
|
||||||
Role: {}",
|
Role: {}",
|
||||||
response.access_token,
|
response.username,
|
||||||
response.token_type,
|
|
||||||
response.expires_in,
|
|
||||||
response.user_id,
|
response.user_id,
|
||||||
response.role
|
response.role
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use the helper method to configure and show the dialog
|
|
||||||
// TODO Implement logic for pressing menu or exit buttons, not imeplementing it now,
|
|
||||||
// need to do other more important stuff now"
|
|
||||||
app_state.show_dialog(
|
app_state.show_dialog(
|
||||||
"Login Success",
|
"Login Success",
|
||||||
&success_message,
|
&success_message,
|
||||||
|
|||||||
@@ -35,4 +35,5 @@ message LoginResponse {
|
|||||||
int32 expires_in = 3; // Expiration in seconds (86400 for 24 hours)
|
int32 expires_in = 3; // Expiration in seconds (86400 for 24 hours)
|
||||||
string user_id = 4; // User's UUID in string format
|
string user_id = 4; // User's UUID in string format
|
||||||
string role = 5; // User's role
|
string role = 5; // User's role
|
||||||
|
string username = 6;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -52,6 +52,8 @@ pub struct LoginResponse {
|
|||||||
/// User's role
|
/// User's role
|
||||||
#[prost(string, tag = "5")]
|
#[prost(string, tag = "5")]
|
||||||
pub role: ::prost::alloc::string::String,
|
pub role: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "6")]
|
||||||
|
pub username: ::prost::alloc::string::String,
|
||||||
}
|
}
|
||||||
/// Generated client implementations.
|
/// Generated client implementations.
|
||||||
pub mod auth_service_client {
|
pub mod auth_service_client {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub async fn login(
|
|||||||
) -> Result<Response<LoginResponse>, Status> {
|
) -> Result<Response<LoginResponse>, Status> {
|
||||||
let user = sqlx::query!(
|
let user = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, password_hash, role
|
SELECT id, username, password_hash, role
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = $1 OR email = $1
|
WHERE username = $1 OR email = $1
|
||||||
"#,
|
"#,
|
||||||
@@ -33,7 +33,7 @@ pub async fn login(
|
|||||||
return Err(Status::unauthenticated("Invalid credentials"));
|
return Err(Status::unauthenticated("Invalid credentials"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = jwt::generate_token(user.id, &user.role)
|
let token = jwt::generate_token(user.id, &user.role, &user.username)
|
||||||
.map_err(|e| Status::internal(e.to_string()))?;
|
.map_err(|e| Status::internal(e.to_string()))?;
|
||||||
|
|
||||||
Ok(Response::new(LoginResponse {
|
Ok(Response::new(LoginResponse {
|
||||||
@@ -42,5 +42,6 @@ pub async fn login(
|
|||||||
expires_in: 86400, // 24 hours
|
expires_in: 86400, // 24 hours
|
||||||
user_id: user.id.to_string(),
|
user_id: user.id.to_string(),
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
username: user.username,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub struct Claims {
|
|||||||
pub sub: Uuid, // User ID
|
pub sub: Uuid, // User ID
|
||||||
pub exp: i64, // Expiration time
|
pub exp: i64, // Expiration time
|
||||||
pub role: String, // User role
|
pub role: String, // User role
|
||||||
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_jwt() -> Result<(), AuthError> {
|
pub fn init_jwt() -> Result<(), AuthError> {
|
||||||
@@ -32,7 +33,7 @@ pub fn init_jwt() -> Result<(), AuthError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_token(user_id: Uuid, role: &str) -> Result<String, AuthError> {
|
pub fn generate_token(user_id: Uuid, role: &str, username: &str) -> Result<String, AuthError> {
|
||||||
let keys = KEYS.get().ok_or(AuthError::ConfigError("JWT not initialized".to_string()))?;
|
let keys = KEYS.get().ok_or(AuthError::ConfigError("JWT not initialized".to_string()))?;
|
||||||
|
|
||||||
let exp = OffsetDateTime::now_utc() + Duration::days(365000);
|
let exp = OffsetDateTime::now_utc() + Duration::days(365000);
|
||||||
@@ -40,6 +41,7 @@ pub fn generate_token(user_id: Uuid, role: &str) -> Result<String, AuthError> {
|
|||||||
sub: user_id,
|
sub: user_id,
|
||||||
exp: exp.unix_timestamp(),
|
exp: exp.unix_timestamp(),
|
||||||
role: role.to_string(),
|
role: role.to_string(),
|
||||||
|
username: username.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
encode(&Header::default(), &claims, &keys.encoding)
|
encode(&Header::default(), &claims, &keys.encoding)
|
||||||
|
|||||||
Reference in New Issue
Block a user