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",
|
||||
"tonic",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -19,4 +19,5 @@ tokio = { version = "1.43.0", features = ["full", "macros"] }
|
||||
toml = "0.8.20"
|
||||
tonic = "0.12.3"
|
||||
tracing = "0.1.41"
|
||||
unicode-segmentation = "1.12.0"
|
||||
unicode-width = "0.2.0"
|
||||
|
||||
@@ -51,11 +51,11 @@ exit_edit_mode = ["esc","ctrl+e"]
|
||||
delete_char_forward = ["delete"]
|
||||
delete_char_backward = ["backspace"]
|
||||
next_field = ["enter"]
|
||||
prev_field = ["backtab"]
|
||||
prev_field = ["shift+enter"]
|
||||
move_left = ["left"]
|
||||
move_right = ["right"]
|
||||
suggestion_down = ["shift+tab"]
|
||||
suggestion_up = ["tab"]
|
||||
suggestion_down = ["ctrl+n", "tab"]
|
||||
suggestion_up = ["ctrl+p", "shift+tab"]
|
||||
select_suggestion = ["enter"]
|
||||
exit_suggestion_mode = ["esc"]
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
components::common::{dialog, autocomplete},
|
||||
state::state::AppState,
|
||||
state::canvas_state::CanvasState,
|
||||
modes::handlers::mode_manager::AppMode,
|
||||
};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
||||
@@ -41,6 +42,7 @@ pub fn render_register(
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(7), // Form (5 fields + padding)
|
||||
Constraint::Length(1), // Help text line
|
||||
Constraint::Length(1), // Error message
|
||||
Constraint::Length(3), // Buttons
|
||||
])
|
||||
@@ -53,25 +55,31 @@ pub fn render_register(
|
||||
state, // The state object (RegisterState)
|
||||
&[ // Field labels
|
||||
"Username",
|
||||
"Email (Optional)",
|
||||
"Password (Optional)",
|
||||
"Email*",
|
||||
"Password*",
|
||||
"Confirm Password",
|
||||
"Role (Optional)",
|
||||
"Role* (Tab)",
|
||||
],
|
||||
&state.current_field(), // Pass current field index
|
||||
&state.inputs().iter().map(|s| *s).collect::<Vec<&String>>(), // Pass inputs directly
|
||||
theme,
|
||||
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 ---
|
||||
if let Some(err) = &state.error_message {
|
||||
f.render_widget(
|
||||
Paragraph::new(err.as_str())
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.alignment(Alignment::Center),
|
||||
chunks[1],
|
||||
chunks[2],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +87,7 @@ pub fn render_register(
|
||||
let button_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[2]);
|
||||
.split(chunks[3]);
|
||||
|
||||
// Register Button
|
||||
let register_button_index = 0;
|
||||
@@ -136,11 +144,13 @@ pub fn render_register(
|
||||
);
|
||||
|
||||
// --- Render Autocomplete Dropdown (Draw AFTER buttons) ---
|
||||
if let Some(suggestions) = state.get_suggestions() {
|
||||
let selected = state.get_selected_suggestion_index();
|
||||
if !suggestions.is_empty() {
|
||||
if let Some(input_rect) = active_field_rect {
|
||||
autocomplete::render_autocomplete_dropdown(f, input_rect, f.size(), theme, suggestions, selected);
|
||||
if app_state.current_mode == AppMode::Edit {
|
||||
if let Some(suggestions) = state.get_suggestions() {
|
||||
let selected = state.get_selected_suggestion_index();
|
||||
if !suggestions.is_empty() {
|
||||
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()
|
||||
.map(|(i, s)| {
|
||||
let is_selected = selected_index == Some(i);
|
||||
ListItem::new(s.as_str()).style(if is_selected {
|
||||
// Style for selected item (highlight background)
|
||||
let s_width = s.width() as u16;
|
||||
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()
|
||||
.fg(theme.bg) // Text color on highlight
|
||||
.bg(theme.highlight) // Highlight background
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// src/components/common/dialog.rs
|
||||
use crate::config::colors::themes::Theme;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||
prelude::Alignment,
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, BorderType, Borders, Paragraph, Clear}, // Added Clear
|
||||
widgets::{Block, BorderType, Borders, Paragraph, Clear},
|
||||
Frame,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation; // For grapheme clusters
|
||||
use unicode_width::UnicodeWidthStr; // For accurate width calculation
|
||||
|
||||
pub fn render_dialog(
|
||||
f: &mut Frame,
|
||||
@@ -18,15 +19,16 @@ pub fn render_dialog(
|
||||
dialog_buttons: &[String],
|
||||
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_height = message_lines.len() as u16;
|
||||
let button_row_height = if dialog_buttons.is_empty() { 0 } else { 3 };
|
||||
let vertical_padding = 2; // Block borders (top/bottom)
|
||||
let inner_vertical_margin = 2; // Margin inside block (top/bottom)
|
||||
|
||||
// Calculate required height based on actual message lines
|
||||
let required_inner_height =
|
||||
message_height + button_row_height + inner_vertical_margin;
|
||||
// Add block border height
|
||||
let required_total_height = required_inner_height + vertical_padding;
|
||||
|
||||
// Use a fixed percentage width, clamped to min/max
|
||||
@@ -61,10 +63,10 @@ pub fn render_dialog(
|
||||
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![
|
||||
// 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 {
|
||||
constraints.push(Constraint::Length(button_row_height));
|
||||
@@ -76,15 +78,39 @@ pub fn render_dialog(
|
||||
.split(inner_area);
|
||||
|
||||
// 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
|
||||
.into_iter()
|
||||
.map(|l| Line::from(Span::styled(l, Style::default().fg(theme.fg))))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
.map(|line| {
|
||||
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 =
|
||||
Paragraph::new(message_text).alignment(Alignment::Center);
|
||||
Paragraph::new(Text::from(processed_lines)).alignment(Alignment::Center);
|
||||
// Render message in the first chunk
|
||||
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 });
|
||||
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 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) });
|
||||
@@ -336,10 +336,12 @@ pub async fn execute_edit_action<S: CanvasState + Any + Send>(
|
||||
register_state.in_suggestion_mode = false;
|
||||
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 {
|
||||
// Action received but not applicable to the current field
|
||||
Ok("".to_string())
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::state::pages::form::FormState;
|
||||
use crate::functions::modes::edit::{auth_e, form_e};
|
||||
use crate::modes::handlers::event::EventOutcome;
|
||||
use crate::state::state::AppState;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
pub async fn handle_edit_event(
|
||||
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) {
|
||||
// --- Special Handling for Tab/Shift+Tab in Role Field ---
|
||||
if app_state.ui.show_register && register_state.current_field() == 4 {
|
||||
match action {
|
||||
"suggestion_up" | "suggestion_down" => { // Mapped to Tab/Shift+Tab
|
||||
if !register_state.in_suggestion_mode {
|
||||
register_state.update_role_suggestions();
|
||||
if !register_state.role_suggestions.is_empty() {
|
||||
register_state.in_suggestion_mode = true;
|
||||
register_state.selected_suggestion_index = Some(0);
|
||||
return Ok("Suggestions shown".to_string());
|
||||
} else {
|
||||
return Ok("No suggestions available".to_string());
|
||||
}
|
||||
}
|
||||
if !register_state.in_suggestion_mode && key.code == KeyCode::Tab && key.modifiers == KeyModifiers::NONE {
|
||||
register_state.update_role_suggestions();
|
||||
if !register_state.role_suggestions.is_empty() {
|
||||
register_state.in_suggestion_mode = true;
|
||||
register_state.selected_suggestion_index = Some(0); // Select first suggestion
|
||||
return Ok("Suggestions shown".to_string());
|
||||
} else {
|
||||
return Ok("No suggestions available".to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// --- End Special Handling ---
|
||||
|
||||
|
||||
return if app_state.ui.show_login {
|
||||
auth_e::execute_edit_action(
|
||||
action,
|
||||
|
||||
@@ -11,21 +11,28 @@ lazy_static! {
|
||||
];
|
||||
}
|
||||
|
||||
/// Represents the authenticated session state
|
||||
#[derive(Default)]
|
||||
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 user_id: 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,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)] // Add Clone derive
|
||||
/// Represents the state of the Registration form UI
|
||||
#[derive(Default, Clone)]
|
||||
pub struct RegisterState {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
@@ -43,23 +50,33 @@ pub struct RegisterState {
|
||||
}
|
||||
|
||||
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 {
|
||||
Self {
|
||||
return_selected: false,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
error_message: None,
|
||||
current_field: 0,
|
||||
current_cursor_pos: 0,
|
||||
auth_token: None,
|
||||
user_id: None,
|
||||
role: None,
|
||||
has_unsaved_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegisterState {
|
||||
/// Creates a new empty RegisterState
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
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) {
|
||||
let current_input = self.role.to_lowercase();
|
||||
self.role_suggestions = AVAILABLE_ROLES
|
||||
@@ -90,7 +107,7 @@ impl RegisterState {
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for AuthState {
|
||||
impl CanvasState for LoginState {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
@@ -124,7 +141,7 @@ impl CanvasState for AuthState {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
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) {
|
||||
if index < 2 { // AuthState only has 2 fields
|
||||
if index < 2 {
|
||||
self.current_field = index;
|
||||
// IMPORTANT: Clamp cursor position to the length of the NEW field
|
||||
let len = match self.current_field {
|
||||
0 => self.username.len(),
|
||||
1 => self.password.len(),
|
||||
_ => 0,
|
||||
0 => self.username.len(),
|
||||
1 => self.password.len(),
|
||||
_ => 0,
|
||||
};
|
||||
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) {
|
||||
let len = match self.current_field {
|
||||
0 => self.username.len(),
|
||||
1 => self.password.len(),
|
||||
_ => 0,
|
||||
0 => self.username.len(),
|
||||
1 => self.password.len(),
|
||||
_ => 0,
|
||||
};
|
||||
// Ensure stored position is always valid
|
||||
self.current_cursor_pos = pos.min(len);
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
// Allow the generic handler to signal changes
|
||||
self.has_unsaved_changes = changed;
|
||||
}
|
||||
|
||||
// --- Autocomplete Support (Not Used for AuthState) ---
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
None // AuthState doesn't provide suggestions
|
||||
None
|
||||
}
|
||||
|
||||
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)",
|
||||
"Password (Optional)",
|
||||
"Confirm Password",
|
||||
"Role (Oprional)"
|
||||
"Role (Optional)"
|
||||
]
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
if index < 5 { // RegisterState has 5 fields
|
||||
if index < 5 {
|
||||
self.current_field = index;
|
||||
let len = match self.current_field {
|
||||
0 => self.username.len(),
|
||||
@@ -265,7 +278,6 @@ impl CanvasState for RegisterState {
|
||||
}
|
||||
|
||||
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 {
|
||||
Some(&self.role_suggestions)
|
||||
} else {
|
||||
|
||||
@@ -28,25 +28,19 @@ pub async fn save(
|
||||
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());
|
||||
auth_state.set_has_unsaved_changes(false);
|
||||
|
||||
let success_message = format!(
|
||||
"Login Successful!\n\n\
|
||||
Access Token: {}\n\
|
||||
Token Type: {}\n\
|
||||
Expires In: {}\n\
|
||||
Username: {}\n\
|
||||
User ID: {}\n\
|
||||
Role: {}",
|
||||
response.access_token,
|
||||
response.token_type,
|
||||
response.expires_in,
|
||||
response.username,
|
||||
response.user_id,
|
||||
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(
|
||||
"Login Success",
|
||||
&success_message,
|
||||
|
||||
@@ -35,4 +35,5 @@ message LoginResponse {
|
||||
int32 expires_in = 3; // Expiration in seconds (86400 for 24 hours)
|
||||
string user_id = 4; // User's UUID in string format
|
||||
string role = 5; // User's role
|
||||
string username = 6;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -52,6 +52,8 @@ pub struct LoginResponse {
|
||||
/// User's role
|
||||
#[prost(string, tag = "5")]
|
||||
pub role: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "6")]
|
||||
pub username: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod auth_service_client {
|
||||
|
||||
@@ -11,7 +11,7 @@ pub async fn login(
|
||||
) -> Result<Response<LoginResponse>, Status> {
|
||||
let user = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, password_hash, role
|
||||
SELECT id, username, password_hash, role
|
||||
FROM users
|
||||
WHERE username = $1 OR email = $1
|
||||
"#,
|
||||
@@ -33,7 +33,7 @@ pub async fn login(
|
||||
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()))?;
|
||||
|
||||
Ok(Response::new(LoginResponse {
|
||||
@@ -42,5 +42,6 @@ pub async fn login(
|
||||
expires_in: 86400, // 24 hours
|
||||
user_id: user.id.to_string(),
|
||||
role: user.role,
|
||||
username: user.username,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct Claims {
|
||||
pub sub: Uuid, // User ID
|
||||
pub exp: i64, // Expiration time
|
||||
pub role: String, // User role
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
pub fn init_jwt() -> Result<(), AuthError> {
|
||||
@@ -32,7 +33,7 @@ pub fn init_jwt() -> Result<(), AuthError> {
|
||||
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 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,
|
||||
exp: exp.unix_timestamp(),
|
||||
role: role.to_string(),
|
||||
username: username.to_string(),
|
||||
};
|
||||
|
||||
encode(&Header::default(), &claims, &keys.encoding)
|
||||
|
||||
Reference in New Issue
Block a user