Compare commits

..

8 Commits

Author SHA1 Message Date
filipriec
1dd5f685a6 authstate splitting into login state and auth state, quite before maddness 2025-04-13 17:06:59 +02:00
filipriec
c1c4394f94 ... at the end of the dialog truncation 2025-04-13 14:58:41 +02:00
filipriec
16d9fcdadc only important stuff in the response of the login 2025-04-13 14:09:57 +02:00
filipriec
5b1db01fe6 displaying username and username contained in the jwt now 2025-04-13 14:04:30 +02:00
filipriec
e856e9d6c7 response contains username but jwt is holding username also 2025-04-13 13:45:22 +02:00
filipriec
ad2c783870 improvements on the register.rs 2025-04-13 00:09:12 +02:00
filipriec
5e101bef14 tab is triggering the suggestion dropdown menu, but ctrl+n and ctrl+p only cycle through it 2025-04-12 23:56:14 +02:00
filipriec
149949ad99 better dropdown gui 2025-04-12 22:42:06 +02:00
15 changed files with 136 additions and 87 deletions

1
Cargo.lock generated
View File

@@ -436,6 +436,7 @@ dependencies = [
"toml", "toml",
"tonic", "tonic",
"tracing", "tracing",
"unicode-segmentation",
"unicode-width 0.2.0", "unicode-width 0.2.0",
] ]

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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);
}
} }
} }
} }

View File

@@ -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

View File

@@ -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(
} }
} }
} }

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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,
})) }))
} }

View File

@@ -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)