Compare commits

..

39 Commits

Author SHA1 Message Date
filipriec
e36b1817bc working, split config where functions for each page are defined for this page, if the functions are not general for each page. Huge update, works for tui/functions/form and fui/functions/login. Working, time to move to other things 2025-03-30 15:46:49 +02:00
filipriec
13d4db6bdc properly handling up and down in the form and login, login needs logic implementation 2025-03-30 15:11:02 +02:00
filipriec
b5a5ebd7c0 working exactly as i want, now making the login up and down to work properly well 2025-03-30 15:03:12 +02:00
filipriec
6e04c1f267 original form fully working in the tui functions is the logic for the form 2025-03-30 14:24:12 +02:00
filipriec
a0d96cb87a moving to tui/functions/form.rs read_only functions 2025-03-30 14:13:07 +02:00
filipriec
4052a5b81b fixed unused imports 2025-03-30 14:01:08 +02:00
filipriec
ed99ebf541 compiled if statement in the read_only mode 2025-03-30 13:57:22 +02:00
filipriec
0a4f59cf8e reverting back fully 2025-03-30 13:15:51 +02:00
filipriec
fd6a9b73be reverting back 2025-03-30 13:11:04 +02:00
filipriec
dc994f6ee1 login in the functions 2025-03-30 13:04:48 +02:00
filipriec
e234dd1785 down and up now added to work in the original form 2025-03-30 12:09:20 +02:00
filipriec
6e0943f0cc compiled 2025-03-30 00:51:11 +01:00
filipriec
9622e0bd3c moving read_only functions for the form specific 2025-03-29 22:36:03 +01:00
filipriec
ee666e91ed adjusted login not working yet, wrong changes 2025-03-29 00:09:06 +01:00
filipriec
4f8f2f4a40 step4 2025-03-28 20:48:06 +01:00
filipriec
f21953147b step 3 compiled 2025-03-28 14:35:16 +01:00
filipriec
48b2658b55 step2 2025-03-28 14:29:36 +01:00
filipriec
37b08fdd10 implementation of canvas for multiple pages step 1 2025-03-28 14:26:18 +01:00
filipriec
d4efdf4833 login improvements 2025-03-27 12:27:43 +01:00
filipriec
b408abe8c6 quick prod check .sqlx 2025-03-26 00:50:25 +01:00
filipriec
722c63af54 fixed the navigation previous function, therefore we are now moving into the login properly, lets implement the auth now 2025-03-26 00:46:59 +01:00
filipriec
4601ba4094 unused stuff removed 2025-03-26 00:43:42 +01:00
filipriec
3e2b8a36df fixing warnings 2025-03-26 00:39:27 +01:00
filipriec
11214734ae functions are now in the file dedicated to the functions belonging to the page 2025-03-26 00:26:36 +01:00
filipriec
92045a4e67 unused stuff 2025-03-26 00:16:38 +01:00
filipriec
9564bd8524 login page is now being properly displayed 2025-03-26 00:04:30 +01:00
filipriec
ad64df2ec3 nothing is fixed, it doesnt work, i cant see a login page 2025-03-25 23:23:47 +01:00
filipriec
aab11c1cba login logic in the components 2025-03-25 22:56:24 +01:00
filipriec
1fe139e0c5 we compiled 2025-03-25 22:01:09 +01:00
filipriec
4ced1a36d4 moved form.rs into the state where it really belongs to 2025-03-25 21:23:52 +01:00
filipriec
45fff34c4c login page being implemented slowly 2025-03-25 16:02:23 +01:00
filipriec
c84fa4a692 frontend implementing login 2025-03-25 15:57:45 +01:00
filipriec
eba3f56ba3 indefinite jwt expiration set 2025-03-25 13:17:13 +01:00
filipriec
71ab588c16 tonic rbac to tower 2025-03-25 12:36:31 +01:00
filipriec
195375c083 temp disable of the rbac 2025-03-25 12:35:10 +01:00
filipriec
34dafcc23e rbac using tonic 2025-03-25 11:33:14 +01:00
filipriec
507f86fcf1 docs 2025-03-25 10:35:22 +01:00
filipriec
f40654d2c4 it compiled 2025-03-25 10:28:29 +01:00
filipriec
cd32c175a4 jwt implementation and login, not working yet 2025-03-25 10:15:17 +01:00
56 changed files with 1295 additions and 296 deletions

View File

@@ -4,3 +4,5 @@ RUST_DB_USER=multi_psql_dev
RUST_DB_PASSWORD=3 RUST_DB_PASSWORD=3
RUST_DB_HOST=localhost RUST_DB_HOST=localhost
RUST_DB_PORT=5432 RUST_DB_PORT=5432
JWT_SECRET=<YOUR-JWT-HERE>

72
Cargo.lock generated
View File

@@ -170,9 +170,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.87" version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -421,8 +421,9 @@ dependencies = [
[[package]] [[package]]
name = "client" name = "client"
version = "0.2.0" version = "0.2.5"
dependencies = [ dependencies = [
"async-trait",
"common", "common",
"crossterm", "crossterm",
"dirs 6.0.0", "dirs 6.0.0",
@@ -457,7 +458,7 @@ dependencies = [
[[package]] [[package]]
name = "common" name = "common"
version = "0.2.0" version = "0.2.5"
dependencies = [ dependencies = [
"prost", "prost",
"serde", "serde",
@@ -1024,8 +1025,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1541,6 +1544,21 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64",
"js-sys",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]] [[package]]
name = "lasso" name = "lasso"
version = "0.7.3" version = "0.7.3"
@@ -1928,6 +1946,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pem"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
dependencies = [
"base64",
"serde",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@@ -2345,6 +2373,20 @@ dependencies = [
"tstr", "tstr",
] ]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.7" version = "0.9.7"
@@ -2547,13 +2589,14 @@ dependencies = [
[[package]] [[package]]
name = "server" name = "server"
version = "0.2.0" version = "0.2.5"
dependencies = [ dependencies = [
"bcrypt", "bcrypt",
"chrono", "chrono",
"common", "common",
"dashmap", "dashmap",
"dotenvy", "dotenvy",
"jsonwebtoken",
"lazy_static", "lazy_static",
"prost", "prost",
"regex", "regex",
@@ -2641,6 +2684,18 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "simple_asn1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 2.0.12",
"time",
]
[[package]] [[package]]
name = "sized-chunks" name = "sized-chunks"
version = "0.6.5" version = "0.6.5"
@@ -3521,6 +3576,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@@ -3551,6 +3612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"serde",
] ]
[[package]] [[package]]

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package] [workspace.package]
# TODO: idk how to do the name, fix later # TODO: idk how to do the name, fix later
# name = "Multieko2" # name = "Multieko2"
version = "0.2.0" version = "0.2.5"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
authors = ["Filip Priečinský <filippriec@gmail.com>"] authors = ["Filip Priečinský <filippriec@gmail.com>"]

View File

@@ -5,6 +5,7 @@ edition.workspace = true
license.workspace = true license.workspace = true
[dependencies] [dependencies]
async-trait = "0.1.88"
common = { path = "../common" } common = { path = "../common" }
crossterm = "0.28.1" crossterm = "0.28.1"

View File

@@ -0,0 +1,5 @@
// src/components/form.rs
pub mod login;
pub mod register;
pub use login::*;

View File

@@ -0,0 +1,126 @@
// src/components/auth/login.rs
use crate::{
components::form::form::render_generic_form,
config::colors::themes::Theme,
state::pages::auth::AuthState,
};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
style::{Color, Style},
widgets::{Block, BorderType, Borders, Paragraph},
Frame,
};
pub fn render_login(
f: &mut Frame,
area: Rect,
theme: &Theme,
state: &AuthState,
) {
// Main login block with plain borders (matches main form style)
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain) // Matches main form style
.border_style(Style::default().fg(theme.border))
.title(" Login ")
.style(Style::default().bg(theme.bg));
f.render_widget(block, area);
let inner_area = area.inner(Margin {
horizontal: 1,
vertical: 1,
});
// Define field names
let fields = &["Username/Email", "Password"];
// Split layout for form and buttons
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(3), // Form area
Constraint::Length(1), // Error message area
Constraint::Length(3), // Buttons area
])
.split(inner_area);
// Render form with plaintext display
render_generic_form(
f,
chunks[0],
"Login",
state,
fields,
theme,
!state.return_selected, // is_edit_mode
);
// Render buttons
let button_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[2]);
// Login button
let login_style = if !state.return_selected {
Style::default()
.fg(theme.highlight)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
Style::default().fg(theme.fg)
};
let login_border_style = if !state.return_selected {
Style::default().fg(theme.accent)
} else {
Style::default().fg(theme.border)
};
f.render_widget(
Paragraph::new("Login")
.style(login_style)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(login_border_style),
),
button_chunks[0],
);
// Return button
let return_style = if state.return_selected {
Style::default()
.fg(theme.highlight)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
Style::default().fg(theme.fg)
};
let return_border_style = if state.return_selected {
Style::default().fg(theme.accent)
} else {
Style::default().fg(theme.border)
};
f.render_widget(
Paragraph::new("Return")
.style(return_style)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(return_border_style),
),
button_chunks[1],
);
// Render error message if present
if let Some(err) = &state.error_message {
let err_block = Paragraph::new(err.as_str())
.style(Style::default().fg(Color::Red))
.alignment(Alignment::Center);
f.render_widget(err_block, chunks[1]);
}
}

View File

View File

@@ -1,4 +1,4 @@
// src/components/handlers/form.rs // src/components/form/form.rs
use ratatui::{ use ratatui::{
widgets::{Paragraph, Block, Borders}, widgets::{Paragraph, Block, Borders},
layout::{Layout, Constraint, Direction, Rect, Margin, Alignment}, layout::{Layout, Constraint, Direction, Rect, Margin, Alignment},
@@ -6,13 +6,14 @@ use ratatui::{
Frame, Frame,
}; };
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::ui::form::FormState; use crate::state::canvas_state::CanvasState;
use crate::components::handlers::canvas::render_canvas; use crate::components::handlers::canvas::render_canvas;
// Original form renderer (keep for backward compatibility)
pub fn render_form( pub fn render_form(
f: &mut Frame, f: &mut Frame,
area: Rect, area: Rect,
form_state: &FormState, form_state: &impl CanvasState,
fields: &[&str], fields: &[&str],
current_field: &usize, current_field: &usize,
inputs: &[&String], inputs: &[&String],
@@ -64,3 +65,47 @@ pub fn render_form(
is_edit_mode, is_edit_mode,
); );
} }
// New generic form renderer
pub fn render_generic_form(
f: &mut Frame,
area: Rect,
title: &str,
state: &impl CanvasState,
fields: &[&str],
theme: &Theme,
is_edit_mode: bool,
) {
// Create form card
let form_card = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.title(format!(" {} ", title))
.style(Style::default().bg(theme.bg).fg(theme.fg));
f.render_widget(form_card, area);
// Define inner area
let inner_area = area.inner(Margin {
horizontal: 1,
vertical: 1,
});
// Create main layout
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1)])
.split(inner_area);
// Delegate to render_canvas
render_canvas(
f,
main_layout[0],
state,
fields,
&state.current_field(),
&state.inputs(),
theme,
is_edit_mode,
);
}

View File

@@ -8,12 +8,12 @@ use ratatui::{
prelude::Alignment, prelude::Alignment,
}; };
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::ui::form::FormState; use crate::state::canvas_state::CanvasState;
pub fn render_canvas( pub fn render_canvas(
f: &mut Frame, f: &mut Frame,
area: Rect, area: Rect,
form_state: &FormState, form_state: &impl CanvasState,
fields: &[&str], fields: &[&str],
current_field: &usize, current_field: &usize,
inputs: &[&String], inputs: &[&String],
@@ -30,7 +30,7 @@ pub fn render_canvas(
let input_container = Block::default() let input_container = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(if is_edit_mode { .border_style(if is_edit_mode {
form_state.has_unsaved_changes.then(|| theme.warning).unwrap_or(theme.accent) form_state.has_unsaved_changes().then(|| theme.warning).unwrap_or(theme.accent)
} else { } else {
theme.secondary theme.secondary
}) })
@@ -81,7 +81,7 @@ pub fn render_canvas(
f.render_widget(input_display, input_rows[i]); f.render_widget(input_display, input_rows[i]);
if is_active { if is_active {
let cursor_x = input_rows[i].x + form_state.current_cursor_pos as u16; let cursor_x = input_rows[i].x + form_state.current_cursor_pos() as u16;
let cursor_y = input_rows[i].y; let cursor_y = input_rows[i].y;
f.set_cursor_position((cursor_x, cursor_y)); f.set_cursor_position((cursor_x, cursor_y));
} }

View File

@@ -1,4 +1,4 @@
// src/components/handlers/intro.rs // src/components/intro/intro.rs
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::Style, style::Style,
@@ -33,7 +33,7 @@ impl IntroState {
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Percentage(35), Constraint::Percentage(35),
Constraint::Length(5), Constraint::Length(7), // Increased to accommodate 3 buttons
Constraint::Percentage(35), Constraint::Percentage(35),
]) ])
.split(inner_area); .split(inner_area);
@@ -48,10 +48,14 @@ impl IntroState {
.alignment(Alignment::Center); .alignment(Alignment::Center);
f.render_widget(title_para, chunks[1]); f.render_widget(title_para, chunks[1]);
// Buttons // Buttons - now with 3 options
let button_area = Layout::default() let button_area = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .constraints([
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
])
.split(chunks[1].inner(Margin { .split(chunks[1].inner(Margin {
horizontal: 1, horizontal: 1,
vertical: 1 vertical: 1
@@ -71,6 +75,13 @@ impl IntroState {
self.selected_option == 1, self.selected_option == 1,
theme, theme,
); );
self.render_button(
f,
button_area[2],
"Login",
self.selected_option == 2,
theme,
);
} }
fn render_button(&self, f: &mut Frame, area: Rect, text: &str, selected: bool, theme: &Theme) { fn render_button(&self, f: &mut Frame, area: Rect, text: &str, selected: bool, theme: &Theme) {
@@ -101,10 +112,10 @@ impl IntroState {
} }
pub fn next_option(&mut self) { pub fn next_option(&mut self) {
self.selected_option = (self.selected_option + 1) % 2; self.selected_option = (self.selected_option + 1) % 3;
} }
pub fn previous_option(&mut self) { pub fn previous_option(&mut self) {
self.selected_option = if self.selected_option == 0 { 1 } else { 0 }; self.selected_option = if self.selected_option == 0 { 2 } else { self.selected_option - 1 };
} }
} }

View File

@@ -4,9 +4,11 @@ pub mod intro;
pub mod admin; pub mod admin;
pub mod common; pub mod common;
pub mod form; pub mod form;
pub mod auth;
pub use handlers::*; pub use handlers::*;
pub use intro::*; pub use intro::*;
pub use admin::*; pub use admin::*;
pub use common::*; pub use common::*;
pub use form::*; pub use form::*;
pub use auth::*;

View File

@@ -1,11 +1,9 @@
// src/modes/canvas/common.rs // src/modes/canvas/common.rs
use crossterm::event::{KeyEvent};
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::tui::terminal::grpc_client::GrpcClient; use crate::tui::terminal::grpc_client::GrpcClient;
use crate::tui::terminal::core::TerminalCore; use crate::tui::terminal::core::TerminalCore;
use crate::tui::controls::commands::CommandHandler; use crate::state::pages::form::FormState;
use crate::ui::handlers::form::FormState;
use crate::state::state::AppState; use crate::state::state::AppState;
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest}; use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
@@ -14,7 +12,6 @@ pub async fn handle_core_action(
action: &str, action: &str,
form_state: &mut FormState, form_state: &mut FormState,
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
command_handler: &mut CommandHandler,
terminal: &mut TerminalCore, terminal: &mut TerminalCore,
app_state: &mut AppState, app_state: &mut AppState,
current_position: &mut u64, current_position: &mut u64,

View File

@@ -6,7 +6,7 @@ use crate::tui::terminal::{
grpc_client::GrpcClient, grpc_client::GrpcClient,
}; };
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::ui::handlers::form::FormState; use crate::state::pages::form::FormState;
use crate::modes::canvas::common; use crate::modes::canvas::common;
pub async fn handle_edit_event_internal( pub async fn handle_edit_event_internal(

View File

@@ -1,8 +1,10 @@
// src/modes/handlers/read_only.rs
// src/modes/canvas/read_only.rs
use crossterm::event::{KeyEvent}; use crossterm::event::{KeyEvent};
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::ui::handlers::form::FormState; use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::config::binds::key_sequences::KeySequenceTracker; use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::tui::terminal::grpc_client::GrpcClient; use crate::tui::terminal::grpc_client::GrpcClient;
@@ -14,9 +16,11 @@ enum CharType {
} }
pub async fn handle_read_only_event( pub async fn handle_read_only_event(
app_state: &crate::state::state::AppState,
key: KeyEvent, key: KeyEvent,
config: &Config, config: &Config,
form_state: &mut FormState, form_state: &mut FormState,
auth_state: &mut AuthState,
key_sequence_tracker: &mut KeySequenceTracker, key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
@@ -50,7 +54,24 @@ pub async fn handle_read_only_event(
// Try to match the current sequence against Read-Only mode bindings // Try to match the current sequence against Read-Only mode bindings
if let Some(action) = config.matches_key_sequence_generalized(&sequence) { if let Some(action) = config.matches_key_sequence_generalized(&sequence) {
let result = execute_action( let result = if (action == "previous_entry" || action == "next_entry" ||
action == "move_up" || action == "move_down") && app_state.ui.show_form {
crate::tui::functions::form::handle_action(
action,
form_state,
grpc_client,
current_position,
total_count,
ideal_cursor_column,
).await?
} else if (action == "move_up" || action == "move_down") && app_state.ui.show_login {
crate::tui::functions::login::handle_action(
action,
auth_state,
ideal_cursor_column,
).await?
} else {
execute_action(
action, action,
form_state, form_state,
ideal_cursor_column, ideal_cursor_column,
@@ -59,7 +80,8 @@ pub async fn handle_read_only_event(
current_position, current_position,
total_count, total_count,
grpc_client, grpc_client,
).await?; ).await?
};
key_sequence_tracker.reset(); key_sequence_tracker.reset();
return Ok((false, result)); return Ok((false, result));
} }
@@ -72,7 +94,17 @@ pub async fn handle_read_only_event(
// Since it's not part of a multi-key sequence, check for a direct action // Since it's not part of a multi-key sequence, check for a direct action
if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) { if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) {
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) {
let result = execute_action( let result = if action == "previous_entry" && app_state.ui.show_form {
crate::tui::functions::form::handle_action(
action,
form_state,
grpc_client,
current_position,
total_count,
ideal_cursor_column,
).await?
} else {
execute_action(
action, action,
form_state, form_state,
ideal_cursor_column, ideal_cursor_column,
@@ -81,7 +113,8 @@ pub async fn handle_read_only_event(
current_position, current_position,
total_count, total_count,
grpc_client, grpc_client,
).await?; ).await?
};
key_sequence_tracker.reset(); key_sequence_tracker.reset();
return Ok((false, result)); return Ok((false, result));
} }
@@ -91,7 +124,17 @@ pub async fn handle_read_only_event(
key_sequence_tracker.reset(); key_sequence_tracker.reset();
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) {
let result = execute_action( let result = if action == "previous_entry" && app_state.ui.show_form {
crate::tui::functions::form::handle_action(
action,
form_state,
grpc_client,
current_position,
total_count,
ideal_cursor_column,
).await?
} else {
execute_action(
action, action,
form_state, form_state,
ideal_cursor_column, ideal_cursor_column,
@@ -100,7 +143,8 @@ pub async fn handle_read_only_event(
current_position, current_position,
total_count, total_count,
grpc_client, grpc_client,
).await?; ).await?
};
return Ok((false, result)); return Ok((false, result));
} }
} }
@@ -128,72 +172,20 @@ async fn execute_action(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
match action { match action {
"previous_entry" => { "previous_entry" | "next_entry" => {
let new_position = current_position.saturating_sub(1); // This will only be called when no component is active
if new_position >= 1 {
*current_position = new_position;
match grpc_client.get_adresar_by_position(*current_position).await {
Ok(response) => {
form_state.id = response.id;
form_state.values = vec![
response.firma, response.kz, response.drc,
response.ulica, response.psc, response.mesto,
response.stat, response.banka, response.ucet,
response.skladm, response.ico, response.kontakt,
response.telefon, response.skladu, response.fax,
];
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
form_state.has_unsaved_changes = false;
*command_message = format!("Loaded entry {}", *current_position);
}
Err(e) => {
*command_message = format!("Error loading entry: {}", e);
}
}
key_sequence_tracker.reset(); key_sequence_tracker.reset();
Ok(format!("Navigation prev/next only available in form mode"))
} }
Ok(command_message.clone()) "move_up" | "move_down" => {
} // This will only be called when no component is active
"next_entry" => {
if *current_position <= total_count {
*current_position += 1;
if *current_position <= total_count {
match grpc_client.get_adresar_by_position(*current_position).await {
Ok(response) => {
form_state.id = response.id;
form_state.values = vec![
response.firma, response.kz, response.drc,
response.ulica, response.psc, response.mesto,
response.stat, response.banka, response.ucet,
response.skladm, response.ico, response.kontakt,
response.telefon, response.skladu, response.fax,
];
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
form_state.has_unsaved_changes = false;
*command_message = format!("Loaded entry {}", *current_position);
}
Err(e) => {
*command_message = format!("Error loading entry: {}", e);
}
}
} else {
form_state.reset_to_empty();
form_state.current_field = 0;
form_state.current_cursor_pos = 0;
*ideal_cursor_column = 0;
*command_message = "New entry mode".to_string();
}
key_sequence_tracker.reset(); key_sequence_tracker.reset();
Ok(format!("Navigation up/down only available in form mode"))
} }
Ok(command_message.clone()) "exit_edit_mode" => {
key_sequence_tracker.reset();
command_message.clear();
Ok("".to_string())
} }
"exit_edit_mode" => { "exit_edit_mode" => {
key_sequence_tracker.reset(); key_sequence_tracker.reset();
@@ -215,38 +207,6 @@ async fn execute_action(
} }
Ok("".to_string()) Ok("".to_string())
} }
"move_up" => {
// Change field first
if form_state.current_field == 0 {
form_state.current_field = form_state.fields.len() - 1;
} else {
form_state.current_field = form_state.current_field.saturating_sub(1);
}
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"move_down" => {
// Change field first
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"move_word_next" => { "move_word_next" => {
let current_input = form_state.get_current_input(); let current_input = form_state.get_current_input();
if !current_input.is_empty() { if !current_input.is_empty() {

View File

@@ -3,7 +3,7 @@
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use crate::tui::terminal::grpc_client::GrpcClient; use crate::tui::terminal::grpc_client::GrpcClient;
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::ui::handlers::form::FormState; use crate::state::pages::form::FormState;
use crate::tui::controls::commands::CommandHandler; use crate::tui::controls::commands::CommandHandler;
use crate::tui::terminal::core::TerminalCore; use crate::tui::terminal::core::TerminalCore;
use crate::modes::{ use crate::modes::{

View File

@@ -3,7 +3,8 @@
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::state::state::AppState; use crate::state::state::AppState;
use crate::ui::handlers::form::FormState; use crate::state::pages::form::FormState;
use crate::tui::functions::{intro, admin};
pub async fn handle_navigation_event( pub async fn handle_navigation_event(
key: KeyEvent, key: KeyEvent,
@@ -21,16 +22,11 @@ pub async fn handle_navigation_event(
return Ok((false, String::new())); return Ok((false, String::new()));
} }
"move_down" => { "move_down" => {
let item_count = if app_state.ui.show_intro { move_down(app_state);
2 // Intro options count
} else {
app_state.profile_tree.profiles.len() // Admin panel items
};
move_down(app_state, item_count);
return Ok((false, String::new())); return Ok((false, String::new()));
} }
"next_option" => { "next_option" => {
next_option(app_state, 2); // Intro has 2 options next_option(app_state); // Intro has 2 options
return Ok((false, String::new())); return Ok((false, String::new()));
} }
"previous_option" => { "previous_option" => {
@@ -84,7 +80,7 @@ pub fn move_up(app_state: &mut AppState) {
} }
} }
pub fn move_down(app_state: &mut AppState, item_count: usize) { pub fn move_down(app_state: &mut AppState) {
if app_state.ui.show_intro { if app_state.ui.show_intro {
app_state.ui.intro_state.next_option(); app_state.ui.intro_state.next_option();
} else if app_state.ui.show_admin { } else if app_state.ui.show_admin {
@@ -98,11 +94,12 @@ pub fn move_down(app_state: &mut AppState, item_count: usize) {
} }
} }
pub fn next_option(app_state: &mut AppState, option_count: usize) { pub fn next_option(app_state: &mut AppState) { // Remove option_count parameter
if app_state.ui.show_intro { if app_state.ui.show_intro {
app_state.ui.intro_state.next_option(); app_state.ui.intro_state.next_option();
} else { } else {
// For other screens that might have options // Get option count from state instead of parameter
let option_count = app_state.profile_tree.profiles.len();
app_state.general.current_option = (app_state.general.current_option + 1) % option_count; app_state.general.current_option = (app_state.general.current_option + 1) % option_count;
} }
} }
@@ -111,36 +108,20 @@ pub fn previous_option(app_state: &mut AppState) {
if app_state.ui.show_intro { if app_state.ui.show_intro {
app_state.ui.intro_state.previous_option(); app_state.ui.intro_state.previous_option();
} else { } else {
// For other screens that might have options let option_count = app_state.profile_tree.profiles.len();
if app_state.general.current_option == 0 { app_state.general.current_option = if app_state.general.current_option == 0 {
// We'd need the option count here, but since it's not passed we can't wrap around correctly option_count.saturating_sub(1) // Wrap to last option
// For now, just stay at 0
} else { } else {
app_state.general.current_option -= 1; app_state.general.current_option - 1
} };
} }
} }
pub fn select(app_state: &mut AppState) { pub fn select(app_state: &mut AppState) {
if app_state.ui.show_intro { if app_state.ui.show_intro {
// Handle selection in intro screen intro::handle_intro_selection(app_state);
if app_state.ui.intro_state.selected_option == 0 {
// First option selected - show form
app_state.ui.show_form = true;
app_state.ui.show_admin = false;
} else {
// Second option selected - show admin
app_state.ui.show_form = false;
app_state.ui.show_admin = true;
}
app_state.ui.show_intro = false;
} else if app_state.ui.show_admin { } else if app_state.ui.show_admin {
// Handle selection in admin panel admin::handle_admin_selection(app_state);
let profiles = &app_state.profile_tree.profiles;
if !profiles.is_empty() && app_state.general.selected_item < profiles.len() {
// Set the selected profile
app_state.selected_profile = Some(profiles[app_state.general.selected_item].name.clone());
}
} }
} }

View File

@@ -1,5 +1,5 @@
// src/modes/handlers/event.rs // src/modes/handlers/event.rs
use crossterm::event::{Event, KeyEvent}; use crossterm::event::Event;
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
use crate::tui::terminal::{ use crate::tui::terminal::{
core::TerminalCore, core::TerminalCore,
@@ -7,7 +7,8 @@ use crate::tui::terminal::{
}; };
use crate::tui::controls::commands::CommandHandler; use crate::tui::controls::commands::CommandHandler;
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::ui::handlers::form::FormState; use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::ui::handlers::rat_state::UiStateHandler; use crate::ui::handlers::rat_state::UiStateHandler;
use crate::modes::{ use crate::modes::{
common::{command_mode}, common::{command_mode},
@@ -25,6 +26,7 @@ pub struct EventHandler {
pub edit_mode_cooldown: bool, pub edit_mode_cooldown: bool,
pub ideal_cursor_column: usize, pub ideal_cursor_column: usize,
pub key_sequence_tracker: KeySequenceTracker, pub key_sequence_tracker: KeySequenceTracker,
pub auth_state: AuthState,
} }
impl EventHandler { impl EventHandler {
@@ -37,6 +39,7 @@ impl EventHandler {
edit_mode_cooldown: false, edit_mode_cooldown: false,
ideal_cursor_column: 0, ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(800), key_sequence_tracker: KeySequenceTracker::new(800),
auth_state: AuthState::new(),
} }
} }
@@ -129,7 +132,6 @@ impl EventHandler {
action, action,
form_state, form_state,
grpc_client, grpc_client,
command_handler,
terminal, terminal,
app_state, app_state,
current_position, current_position,
@@ -142,9 +144,11 @@ impl EventHandler {
// Let read_only mode handle its own actions (including navigation from common bindings) // Let read_only mode handle its own actions (including navigation from common bindings)
return read_only::handle_read_only_event( return read_only::handle_read_only_event(
&app_state,
key, key,
config, config,
form_state, form_state,
&mut self.auth_state,
&mut self.key_sequence_tracker, &mut self.key_sequence_tracker,
current_position, current_position,
total_count, total_count,
@@ -188,7 +192,6 @@ impl EventHandler {
action, action,
form_state, form_state,
grpc_client, grpc_client,
command_handler,
terminal, terminal,
app_state, app_state,
current_position, current_position,

View File

@@ -15,14 +15,16 @@ pub struct ModeManager;
impl ModeManager { impl ModeManager {
// Determine current mode based on app state // Determine current mode based on app state
pub fn derive_mode(app_state: &AppState, event_handler: &EventHandler) -> AppMode { pub fn derive_mode(app_state: &AppState, event_handler: &EventHandler) -> AppMode {
// Command mode takes precedence if active
if event_handler.command_mode { if event_handler.command_mode {
return AppMode::Command; return AppMode::Command;
} }
// Check UI state flags if app_state.ui.show_login { // NEW: Check auth visibility
if app_state.ui.show_intro || app_state.ui.show_admin { if event_handler.is_edit_mode {
AppMode::General AppMode::Edit
} else {
AppMode::ReadOnly
}
} else if app_state.ui.show_form { } else if app_state.ui.show_form {
if event_handler.is_edit_mode { if event_handler.is_edit_mode {
AppMode::Edit AppMode::Edit
@@ -30,7 +32,6 @@ impl ModeManager {
AppMode::ReadOnly AppMode::ReadOnly
} }
} else { } else {
// Fallback
AppMode::General AppMode::General
} }
} }

View File

@@ -0,0 +1,44 @@
// src/state/canvas_state.rs
use crate::state::pages::form::FormState;
pub trait CanvasState {
fn current_field(&self) -> usize;
fn current_cursor_pos(&self) -> usize;
fn has_unsaved_changes(&self) -> bool;
fn inputs(&self) -> Vec<&String>;
fn get_current_input(&self) -> &str;
fn get_current_input_mut(&mut self) -> &mut String;
}
// Implement for FormState (keep existing form.rs code and add this)
impl CanvasState for FormState {
fn current_field(&self) -> usize {
self.current_field
}
fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn inputs(&self) -> Vec<&String> {
self.values.iter().collect()
}
fn get_current_input(&self) -> &str {
self.values
.get(self.current_field)
.map(|s| s.as_str())
.unwrap_or("")
}
fn get_current_input_mut(&mut self) -> &mut String {
self.values
.get_mut(self.current_field)
.expect("Invalid current_field index")
}
}

View File

@@ -1,2 +1,4 @@
// src/state/mod.rs // src/state/mod.rs
pub mod state; pub mod state;
pub mod pages;
pub mod canvas_state;

View File

@@ -0,0 +1,4 @@
// src/state/pages.rs
pub mod form;
pub mod auth;

View File

@@ -0,0 +1,60 @@
// src/state/pages/auth.rs
use crate::state::canvas_state::CanvasState;
#[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,
}
impl AuthState {
pub fn new() -> Self {
Self {
return_selected: false,
username: String::new(),
password: String::new(),
error_message: None,
current_field: 0,
current_cursor_pos: 0,
}
}
}
impl CanvasState for AuthState {
fn current_field(&self) -> usize {
self.current_field
}
fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos
}
fn has_unsaved_changes(&self) -> bool {
// Auth form doesn't need unsaved changes tracking
false
}
fn inputs(&self) -> Vec<&String> {
vec![&self.username, &self.password]
}
fn get_current_input(&self) -> &str {
match self.current_field {
0 => &self.username,
1 => &self.password,
_ => "", // Return empty string for invalid index instead of panicking
}
}
fn get_current_input_mut(&mut self) -> &mut String {
match self.current_field {
0 => &mut self.username,
1 => &mut self.password,
_ => panic!("Invalid current_field index in AuthState"),
}
}
}

View File

@@ -1,4 +1,4 @@
// src/client/ui/handlers/form.rs // src/state/pages/form.rs
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::Frame; use ratatui::Frame;
@@ -70,4 +70,15 @@ impl FormState {
.get_mut(self.current_field) .get_mut(self.current_field)
.expect("Invalid current_field index") .expect("Invalid current_field index")
} }
pub fn update_from_response(&mut self, response: common::proto::multieko2::adresar::AdresarResponse) {
self.id = response.id;
self.values = vec![
response.firma, response.kz, response.drc,
response.ulica, response.psc, response.mesto,
response.stat, response.banka, response.ucet,
response.skladm, response.ico, response.kontakt,
response.telefon, response.skladu, response.fax,
];
}
} }

View File

@@ -11,6 +11,7 @@ pub struct UiState {
pub show_intro: bool, pub show_intro: bool,
pub show_admin: bool, pub show_admin: bool,
pub show_form: bool, pub show_form: bool,
pub show_login: bool,
pub intro_state: IntroState, pub intro_state: IntroState,
} }
@@ -75,6 +76,7 @@ impl Default for UiState {
show_intro: true, show_intro: true,
show_admin: false, show_admin: false,
show_form: false, show_form: false,
show_login: false,
intro_state: IntroState::new(), intro_state: IntroState::new(),
} }
} }

View File

@@ -0,0 +1,10 @@
// src/tui/functions.rs
pub mod admin;
pub mod intro;
pub mod login;
pub mod form;
pub use admin::*;
pub use intro::*;
pub use form::*;

View File

@@ -0,0 +1,8 @@
use crate::state::state::AppState;
pub fn handle_admin_selection(app_state: &mut AppState) {
let profiles = &app_state.profile_tree.profiles;
if !profiles.is_empty() && app_state.general.selected_item < profiles.len() {
app_state.selected_profile = Some(profiles[app_state.general.selected_item].name.clone());
}
}

View File

@@ -0,0 +1,112 @@
// src/tui/functions/form.rs
use crate::state::pages::form::FormState;
use crate::tui::terminal::GrpcClient;
use common::proto::multieko2::adresar::AdresarResponse;
pub async fn handle_action(
action: &str,
form_state: &mut FormState,
grpc_client: &mut GrpcClient,
current_position: &mut u64,
total_count: u64,
ideal_cursor_column: &mut usize,
) -> Result<String, Box<dyn std::error::Error>> {
match action {
"previous_entry" => {
let new_position = current_position.saturating_sub(1);
if new_position >= 1 {
*current_position = new_position;
let response = grpc_client.get_adresar_by_position(*current_position).await?;
// Direct field assignments
form_state.id = response.id;
form_state.values = vec![
response.firma, response.kz, response.drc,
response.ulica, response.psc, response.mesto,
response.stat, response.banka, response.ucet,
response.skladm, response.ico, response.kontakt,
response.telefon, response.skladu, response.fax,
];
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
form_state.has_unsaved_changes = false;
Ok(format!("Loaded form entry {}", *current_position))
} else {
Ok("Already at first form entry".into())
}
}
"next_entry" => {
if *current_position <= total_count {
*current_position += 1;
if *current_position <= total_count {
let response = grpc_client.get_adresar_by_position(*current_position).await?;
// Direct field assignments
form_state.id = response.id;
form_state.values = vec![
response.firma, response.kz, response.drc,
response.ulica, response.psc, response.mesto,
response.stat, response.banka, response.ucet,
response.skladm, response.ico, response.kontakt,
response.telefon, response.skladu, response.fax,
];
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
form_state.has_unsaved_changes = false;
Ok(format!("Loaded form entry {}", *current_position))
} else {
form_state.reset_to_empty();
form_state.current_field = 0;
form_state.current_cursor_pos = 0;
*ideal_cursor_column = 0;
Ok("New form entry mode".into())
}
} else {
Ok("Already at last entry".into())
}
}
"move_up" => {
// Change field first
if form_state.current_field == 0 {
form_state.current_field = form_state.fields.len() - 1;
} else {
form_state.current_field = form_state.current_field.saturating_sub(1);
}
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"move_down" => {
// Change field first
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
_ => Err("Unknown form action".into())
}
}

View File

@@ -0,0 +1,23 @@
use crate::state::state::AppState;
pub fn handle_intro_selection(app_state: &mut AppState) {
match app_state.ui.intro_state.selected_option {
0 => { // Continue
app_state.ui.show_form = true;
app_state.ui.show_admin = false;
app_state.ui.show_login = false;
}
1 => { // Admin
app_state.ui.show_form = false;
app_state.ui.show_admin = true;
app_state.ui.show_login = false;
}
2 => { // Login
app_state.ui.show_form = false;
app_state.ui.show_admin = false;
app_state.ui.show_login = true;
}
_ => {}
}
app_state.ui.show_intro = false;
}

View File

@@ -0,0 +1,58 @@
// src/tui/functions/login.rs
use crate::state::pages::auth::AuthState;
use crate::state::canvas_state::CanvasState;
pub async fn handle_action(
action: &str,
auth_state: &mut AuthState,
ideal_cursor_column: &mut usize,
) -> Result<String, Box<dyn std::error::Error>> {
match action {
"move_up" => {
if auth_state.return_selected {
// Coming from return button to fields
auth_state.return_selected = false;
auth_state.current_field = 1; // Focus on password field
} else if auth_state.current_field == 1 {
// Moving from password to username/email
auth_state.current_field = 0;
} else if auth_state.current_field == 0 {
// Wrap around to buttons
auth_state.return_selected = false; // Select Login button
}
// Update cursor position when in a field
if !auth_state.return_selected {
let current_input = auth_state.get_current_input();
let max_cursor_pos = current_input.len();
auth_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
}
Ok(format!("Navigation 'up' from functions/login"))
},
"move_down" => {
if auth_state.return_selected {
// Coming from return button to fields
auth_state.return_selected = false;
auth_state.current_field = 0; // Focus on username field
} else if auth_state.current_field == 0 {
// Moving from username/email to password
auth_state.current_field = 1;
} else if auth_state.current_field == 1 {
// Moving from password to buttons
auth_state.return_selected = false; // Select Login button
}
// Update cursor position when in a field
if !auth_state.return_selected {
let current_input = auth_state.get_current_input();
let max_cursor_pos = current_input.len();
auth_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
}
Ok(format!("Navigation 'down' from functions/login"))
},
_ => Err("Unknown login action".into())
}
}

View File

@@ -1,4 +1,5 @@
// src/tui/mod.rs // src/tui/mod.rs
pub mod terminal; pub mod terminal;
pub mod controls; pub mod controls;
pub mod functions;

View File

@@ -1,7 +1,6 @@
// src/client/ui/handlers.rs // src/client/ui/handlers.rs
pub mod ui; pub mod ui;
pub mod form;
pub mod render; pub mod render;
pub mod rat_state; pub mod rat_state;

View File

@@ -6,18 +6,20 @@ use crate::components::{
render_status_line, render_status_line,
handlers::sidebar::{self, calculate_sidebar_layout}, handlers::sidebar::{self, calculate_sidebar_layout},
form::form::render_form, form::form::render_form,
intro::{intro},
admin::{admin_panel::AdminPanelState}, admin::{admin_panel::AdminPanelState},
auth::login::render_login,
}; };
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::Frame; use ratatui::Frame;
use super::form::FormState; use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::state::state::AppState; use crate::state::state::AppState;
pub fn render_ui( pub fn render_ui(
f: &mut Frame, f: &mut Frame,
form_state: &mut FormState, form_state: &mut FormState,
auth_state: &mut AuthState,
theme: &Theme, theme: &Theme,
is_edit_mode: bool, is_edit_mode: bool,
total_count: u64, total_count: u64,
@@ -44,6 +46,8 @@ pub fn render_ui(
if app_state.ui.show_intro { if app_state.ui.show_intro {
// Use app_state's intro_state directly // Use app_state's intro_state directly
app_state.ui.intro_state.render(f, main_content_area, theme); app_state.ui.intro_state.render(f, main_content_area, theme);
}else if app_state.ui.show_login {
render_login(f, main_content_area, theme, auth_state);
} else if app_state.ui.show_admin { } else if app_state.ui.show_admin {
// Create temporary AdminPanelState for rendering // Create temporary AdminPanelState for rendering
let mut admin_state = AdminPanelState::new( let mut admin_state = AdminPanelState::new(

View File

@@ -6,11 +6,11 @@ use crate::tui::controls::CommandHandler;
use crate::tui::terminal::EventReader; use crate::tui::terminal::EventReader;
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::ui::handlers::{form::FormState, render::render_ui}; use crate::ui::handlers::render::render_ui;
use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::modes::handlers::event::EventHandler; use crate::modes::handlers::event::EventHandler;
use crate::state::state::AppState; use crate::state::state::AppState;
use crate::components::admin::{admin_panel::AdminPanelState};
use crate::components::intro::{intro::IntroState};
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> { pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?; let config = Config::load()?;
@@ -18,7 +18,7 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let mut grpc_client = GrpcClient::new().await?; let mut grpc_client = GrpcClient::new().await?;
let mut command_handler = CommandHandler::new(); let mut command_handler = CommandHandler::new();
let theme = Theme::from_str(&config.colors.theme); let theme = Theme::from_str(&config.colors.theme);
let mut intro_state = IntroState::new(); let mut auth_state = AuthState::default();
// Initialize app_state first // Initialize app_state first
let mut app_state = AppState::new()?; let mut app_state = AppState::new()?;
@@ -27,13 +27,6 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let profile_tree = grpc_client.get_profile_tree().await?; let profile_tree = grpc_client.get_profile_tree().await?;
app_state.profile_tree = profile_tree; app_state.profile_tree = profile_tree;
// Now create admin panel with profiles from app_state
if intro_state.selected_option == 1 {
app_state.ui.show_admin = true;
app_state.general.selected_item = 0;
app_state.general.current_option = 0;
}
// Fetch table structure at startup (one-time) // Fetch table structure at startup (one-time)
let table_structure = grpc_client.get_table_structure().await?; let table_structure = grpc_client.get_table_structure().await?;
@@ -65,6 +58,7 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
render_ui( render_ui(
f, f,
&mut form_state, &mut form_state,
&mut auth_state,
&theme, &theme,
event_handler.is_edit_mode, event_handler.is_edit_mode,
app_state.total_count, app_state.total_count,

View File

@@ -6,6 +6,7 @@ import "common.proto";
service AuthService { service AuthService {
rpc Register(RegisterRequest) returns (AuthResponse); rpc Register(RegisterRequest) returns (AuthResponse);
rpc Login(LoginRequest) returns (LoginResponse);
} }
message RegisterRequest { message RegisterRequest {
@@ -21,3 +22,16 @@ message AuthResponse {
string email = 3; // Registered email (if provided) string email = 3; // Registered email (if provided)
string role = 4; // Default role: 'accountant' string role = 4; // Default role: 'accountant'
} }
message LoginRequest {
string identifier = 1; // Can be username or email
string password = 2;
}
message LoginResponse {
string access_token = 1; // JWT token
string token_type = 2; // Usually "Bearer"
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
}

Binary file not shown.

View File

@@ -25,6 +25,32 @@ pub struct AuthResponse {
#[prost(string, tag = "4")] #[prost(string, tag = "4")]
pub role: ::prost::alloc::string::String, pub role: ::prost::alloc::string::String,
} }
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LoginRequest {
/// Can be username or email
#[prost(string, tag = "1")]
pub identifier: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub password: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LoginResponse {
/// JWT token
#[prost(string, tag = "1")]
pub access_token: ::prost::alloc::string::String,
/// Usually "Bearer"
#[prost(string, tag = "2")]
pub token_type: ::prost::alloc::string::String,
/// Expiration in seconds (86400 for 24 hours)
#[prost(int32, tag = "3")]
pub expires_in: i32,
/// User's UUID in string format
#[prost(string, tag = "4")]
pub user_id: ::prost::alloc::string::String,
/// User's role
#[prost(string, tag = "5")]
pub role: ::prost::alloc::string::String,
}
/// Generated client implementations. /// Generated client implementations.
pub mod auth_service_client { pub mod auth_service_client {
#![allow( #![allow(
@@ -137,6 +163,27 @@ pub mod auth_service_client {
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Register")); .insert(GrpcMethod::new("multieko2.auth.AuthService", "Register"));
self.inner.unary(req, path, codec).await self.inner.unary(req, path, codec).await
} }
pub async fn login(
&mut self,
request: impl tonic::IntoRequest<super::LoginRequest>,
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.auth.AuthService/Login",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Login"));
self.inner.unary(req, path, codec).await
}
} }
} }
/// Generated server implementations. /// Generated server implementations.
@@ -156,6 +203,10 @@ pub mod auth_service_server {
&self, &self,
request: tonic::Request<super::RegisterRequest>, request: tonic::Request<super::RegisterRequest>,
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status>; ) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status>;
async fn login(
&self,
request: tonic::Request<super::LoginRequest>,
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status>;
} }
#[derive(Debug)] #[derive(Debug)]
pub struct AuthServiceServer<T> { pub struct AuthServiceServer<T> {
@@ -278,6 +329,49 @@ pub mod auth_service_server {
}; };
Box::pin(fut) Box::pin(fut)
} }
"/multieko2.auth.AuthService/Login" => {
#[allow(non_camel_case_types)]
struct LoginSvc<T: AuthService>(pub Arc<T>);
impl<T: AuthService> tonic::server::UnaryService<super::LoginRequest>
for LoginSvc<T> {
type Response = super::LoginResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::LoginRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as AuthService>::login(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = LoginSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => { _ => {
Box::pin(async move { Box::pin(async move {
let mut response = http::Response::new(empty_body()); let mut response = http::Response::new(empty_body());

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ltd.table_name\n FROM table_definition_links tdl\n JOIN table_definitions ltd ON tdl.linked_table_id = ltd.id\n WHERE tdl.source_table_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "table_name",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "468bb5bb4fdaefcb4b280761d7880a556d40c172568ad3a1ed13156fbef72776"
}

View File

@@ -0,0 +1,42 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users (username, email, password_hash, role)\n VALUES ($1, $2, $3, 'accountant')\n RETURNING id, username, email, role\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "role",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": [
false,
false,
true,
false
]
},
"hash": "48d0a6d393dac121bfa4230830a105aede2179b07395f97750ab2fa1970afacd"
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, password_hash, role\n FROM users\n WHERE username = $1 OR email = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "password_hash",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "role",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "6be0bb23ad1ba85add309f6e4f55495a5d248901fb0d23fea908815747a0bd50"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT table_name FROM table_definitions\n WHERE profile_id = $1 AND table_name LIKE $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "table_name",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "80f0f7f9ab12a8fe07ea71b548d27bcd6253b598ac4486b72b3d960f03df47f3"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ltd.table_name \n FROM table_definition_links tdl\n JOIN table_definitions ltd ON tdl.linked_table_id = ltd.id\n WHERE tdl.source_table_id = $1 AND tdl.is_required = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "table_name",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "b097f30f98490b979939759d85327a20ca7ade4866052a5cfdb0451fb76fbf15"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "INSERT INTO table_scripts\n (table_definitions_id, target_column, target_column_type, script, description)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id", "query": "INSERT INTO table_scripts\n (table_definitions_id, target_table, target_column,\n target_column_type, script, description, profile_id)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -15,12 +15,14 @@
"Text", "Text",
"Text", "Text",
"Text", "Text",
"Text" "Text",
"Text",
"Int8"
] ]
}, },
"nullable": [ "nullable": [
false false
] ]
}, },
"hash": "4cd5de4d3332ca35a9975ffb32728041978435e14f97c559e2ffec4a82d567ae" "hash": "c07a8511e5f32bf230240b28cb292d40f862b6ec58883f21ee8e1937860585d6"
} }

View File

@@ -26,7 +26,8 @@ lazy_static = "1.5.0"
regex = "1.11.1" regex = "1.11.1"
bcrypt = "0.17.0" bcrypt = "0.17.0"
validator = { version = "0.20.0", features = ["derive"] } validator = { version = "0.20.0", features = ["derive"] }
uuid = { version = "1.16.0", features = ["v4"] } uuid = { version = "1.16.0", features = ["serde", "v4"] }
jsonwebtoken = "9.3.1"
[lib] [lib]
name = "server" name = "server"

View File

@@ -0,0 +1,51 @@
grpcurl -plaintext -d '{
"username": "testuser3",
"email": "test3@example.com",
"password": "your_password",
"password_confirmation": "your_password"
}' localhost:50051 multieko2.auth.AuthService/Register
{
"id": "96d2fd35-b39d-4c05-916a-66134453d34c",
"username": "testuser3",
"email": "test3@example.com",
"role": "accountant"
}
grpcurl -plaintext -d '{
"identifier": "testuser3"
}' localhost:50051 multieko2.auth.AuthService/Login
ERROR:
Code: Unauthenticated
Message: Invalid credentials
grpcurl -plaintext -d '{
"identifier": "testuser3",
"password": "your_password"
}' localhost:50051 multieko2.auth.AuthService/Login
{
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5NmQyZmQzNS1iMzlkLTRjMDUtOTE2YS02NjEzNDQ1M2QzNGMiLCJleHAiOjE3NDI5ODE2MTAsInJvbGUiOiJhY2NvdW50YW50In0.78VIR3X4QZohzeI5x3xmkmqcICTusOC6PELPohMV-k8",
"tokenType": "Bearer",
"expiresIn": 86400,
"userId": "96d2fd35-b39d-4c05-916a-66134453d34c",
"role": "accountant"
}
grpcurl -plaintext -d '{
"username": "testuser4",
"email": "test4@example.com"
}' localhost:50051 multieko2.auth.AuthService/Register
{
"id": "413d7ecc-f231-48af-8c5a-566b1dc2bf0b",
"username": "testuser4",
"email": "test4@example.com",
"role": "accountant"
}
grpcurl -plaintext -d '{
"identifier": "test4@example.com"
}' localhost:50051 multieko2.auth.AuthService/Login
{
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MTNkN2VjYy1mMjMxLTQ4YWYtOGM1YS01NjZiMWRjMmJmMGIiLCJleHAiOjE3NDI5ODE3MDEsInJvbGUiOiJhY2NvdW50YW50In0.4Hzu3tTZRNGHnBSgeCbGy2tFTl8EzpPdXBhcW8kuIc8",
"tokenType": "Bearer",
"expiresIn": 86400,
"userId": "413d7ecc-f231-48af-8c5a-566b1dc2bf0b",
"role": "accountant"
}
╭─    ~/Doc/pr/multieko2/server    auth ······ ✔
╰─

View File

@@ -1,5 +1,7 @@
// src/auth/handlers.rs // src/auth/handlers.rs
pub mod register; pub mod register;
pub mod login;
pub use register::*; pub use register::*;
pub use login::*;

View File

@@ -0,0 +1,46 @@
// src/auth/handlers/login.rs
use bcrypt::verify;
use tonic::{Request, Response, Status};
use crate::db::PgPool;
use crate::auth::{models::AuthError, logic::jwt}; // Fixed import path
use common::proto::multieko2::auth::{LoginRequest, LoginResponse};
pub async fn login(
pool: &PgPool,
request: LoginRequest,
) -> Result<Response<LoginResponse>, Status> {
let user = sqlx::query!(
r#"
SELECT id, password_hash, role
FROM users
WHERE username = $1 OR email = $1
"#,
request.identifier
)
.fetch_optional(pool)
.await
.map_err(|e| Status::internal(e.to_string()))?
.ok_or_else(|| Status::unauthenticated("Invalid credentials"))?;
// Handle the optional password_hash
let password_hash = user.password_hash
.ok_or_else(|| Status::internal("User account has no password set"))?;
// Verify the password
if !verify(&request.password, &password_hash)
.map_err(|e| Status::internal(e.to_string()))?
{
return Err(Status::unauthenticated("Invalid credentials"));
}
let token = jwt::generate_token(user.id, &user.role)
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(LoginResponse {
access_token: token,
token_type: "Bearer".to_string(),
expires_in: 86400, // 24 hours
user_id: user.id.to_string(),
role: user.role,
}))
}

View File

@@ -1,29 +1,14 @@
// src/auth/handlers/register.rs // src/auth/handlers/register.rs
use bcrypt::{hash, DEFAULT_COST}; use bcrypt::{hash, DEFAULT_COST};
use tonic::{Request, Response, Status}; use tonic::{Response, Status};
use common::proto::multieko2::auth::{auth_service_server, RegisterRequest, AuthResponse}; use common::proto::multieko2::auth::{RegisterRequest, AuthResponse};
use crate::db::PgPool; use crate::db::PgPool;
use crate::auth::models::AuthError; use crate::auth::models::AuthError;
pub struct AuthService { pub async fn register(
pool: PgPool, pool: &PgPool,
} payload: RegisterRequest,
) -> Result<Response<AuthResponse>, Status> {
impl AuthService {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[tonic::async_trait]
impl auth_service_server::AuthService for AuthService {
async fn register(
&self,
request: Request<RegisterRequest>,
) -> Result<Response<AuthResponse>, Status> {
let payload = request.into_inner();
// Validate passwords match // Validate passwords match
if payload.password != payload.password_confirmation { if payload.password != payload.password_confirmation {
return Err(Status::invalid_argument(AuthError::PasswordMismatch.to_string())); return Err(Status::invalid_argument(AuthError::PasswordMismatch.to_string()));
@@ -44,7 +29,7 @@ impl auth_service_server::AuthService for AuthService {
payload.email, payload.email,
password_hash password_hash
) )
.fetch_one(&self.pool) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
if e.to_string().contains("duplicate key") { if e.to_string().contains("duplicate key") {
@@ -60,5 +45,4 @@ impl auth_service_server::AuthService for AuthService {
email: user.email.unwrap_or_default(), email: user.email.unwrap_or_default(),
role: user.role, role: user.role,
})) }))
}
} }

9
server/src/auth/logic.rs Normal file
View File

@@ -0,0 +1,9 @@
// src/auth/logic.rs
pub mod jwt;
pub mod middleware;
// TODO implement RBAC on all of the endpoints
// pub mod rbac;
pub use jwt::*;
pub use middleware::*;

View File

@@ -0,0 +1,55 @@
// src/auth/jwt.rs
use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime};
use uuid::Uuid;
use std::sync::OnceLock;
use crate::auth::models::AuthError;
static KEYS: OnceLock<Keys> = OnceLock::new();
struct Keys {
encoding: EncodingKey,
decoding: DecodingKey,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: Uuid, // User ID
pub exp: i64, // Expiration time
pub role: String, // User role
}
pub fn init_jwt() -> Result<(), AuthError> {
let secret = std::env::var("JWT_SECRET")
.map_err(|_| AuthError::ConfigError("JWT_SECRET must be set".to_string()))?;
KEYS.set(Keys {
encoding: EncodingKey::from_secret(secret.as_bytes()),
decoding: DecodingKey::from_secret(secret.as_bytes()),
}).map_err(|_| AuthError::ConfigError("Failed to initialize JWT keys".to_string()))?;
Ok(())
}
pub fn generate_token(user_id: Uuid, role: &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);
let claims = Claims {
sub: user_id,
exp: exp.unix_timestamp(),
role: role.to_string(),
};
encode(&Header::default(), &claims, &keys.encoding)
.map_err(|e| AuthError::JwtError(e.to_string()))
}
pub fn validate_token(token: &str) -> Result<Claims, AuthError> {
let keys = KEYS.get().ok_or(AuthError::ConfigError("JWT not initialized".to_string()))?;
decode::<Claims>(token, &keys.decoding, &Validation::default())
.map(|data| data.claims)
.map_err(|e| AuthError::JwtError(e.to_string()))
}

View File

@@ -0,0 +1,22 @@
// src/auth/middleware.rs
use tonic::{metadata::MetadataValue, service::Interceptor, Status};
use crate::auth::{logic::jwt, models::AuthError};
pub struct AuthInterceptor;
impl Interceptor for AuthInterceptor {
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
let metadata = request.metadata();
let token = metadata.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.ok_or(Status::unauthenticated("Missing authorization header"))?;
let claims = jwt::validate_token(token)
.map_err(|e| Status::unauthenticated(e.to_string()))?;
// Store claims in request extensions
request.extensions_mut().insert(claims);
Ok(request)
}
}

View File

@@ -0,0 +1,36 @@
// src/auth/logic/rbac.rs
use tower::ServiceBuilder;
use crate::auth::logic::rbac;
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
// ... existing setup code ...
// Create service layers
let adresar_layer = ServiceBuilder::new()
.layer(rbac::create_adresar_layer())
.into_inner();
let uctovnictvo_layer = ServiceBuilder::new()
.layer(rbac::create_uctovnictvo_layer())
.into_inner();
// Create services with layers
let adresar_service = AdresarServer::new(AdresarService { db_pool: db_pool.clone() })
.layer(adresar_layer);
let uctovnictvo_service = UctovnictvoServer::new(UctovnictvoService { db_pool: db_pool.clone() })
.layer(uctovnictvo_layer);
// ... repeat for other services ...
Server::builder()
.add_service(auth_server)
.add_service(adresar_service)
.add_service(uctovnictvo_service)
// ... other services ...
.serve(addr)
.await?;
Ok(())
}

View File

@@ -1,5 +1,6 @@
// src/auth/mod.rs // src/auth/mod.rs
pub mod models; pub mod models;
pub mod logic;
pub mod handlers; pub mod handlers;

View File

@@ -14,6 +14,14 @@ pub struct RegisterRequest {
pub password_confirmation: String, pub password_confirmation: String,
} }
#[derive(Debug, Validate, Deserialize)]
pub struct LoginRequest {
#[validate(length(min = 1))]
pub identifier: String,
#[validate(length(min = 1))]
pub password: String,
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AuthError { pub enum AuthError {
#[error("Passwords do not match")] #[error("Passwords do not match")]
@@ -24,4 +32,10 @@ pub enum AuthError {
DatabaseError(String), DatabaseError(String),
#[error("Hashing error: {0}")] #[error("Hashing error: {0}")]
HashingError(String), HashingError(String),
#[error("Invalid credentials")]
InvalidCredentials,
#[error("JWT error: {0}")]
JwtError(String),
#[error("Configuration error: {0}")]
ConfigError(String),
} }

View File

@@ -10,6 +10,7 @@ use crate::server::services::{
TableDefinitionService, TableDefinitionService,
TablesDataService, TablesDataService,
TableScriptService, TableScriptService,
AuthServiceImpl
}; };
use common::proto::multieko2::{ use common::proto::multieko2::{
adresar::adresar_server::AdresarServer, adresar::adresar_server::AdresarServer,
@@ -18,11 +19,13 @@ use common::proto::multieko2::{
table_definition::table_definition_server::TableDefinitionServer, table_definition::table_definition_server::TableDefinitionServer,
tables_data::tables_data_server::TablesDataServer, tables_data::tables_data_server::TablesDataServer,
table_script::table_script_server::TableScriptServer, table_script::table_script_server::TableScriptServer,
auth::auth_service_server::AuthServiceServer // Add this import auth::auth_service_server::AuthServiceServer
}; };
use crate::auth::handlers::AuthService; // Add this import
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> { pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
// Initialize JWT for authentication
crate::auth::logic::jwt::init_jwt()?;
let addr = "[::1]:50051".parse()?; let addr = "[::1]:50051".parse()?;
let reflection_service = ReflectionBuilder::configure() let reflection_service = ReflectionBuilder::configure()
@@ -33,7 +36,7 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() }; let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
let tables_data_service = TablesDataService { db_pool: db_pool.clone() }; let tables_data_service = TablesDataService { db_pool: db_pool.clone() };
let table_script_service = TableScriptService { db_pool: db_pool.clone() }; let table_script_service = TableScriptService { db_pool: db_pool.clone() };
let auth_service = AuthService::new(db_pool.clone()); // Add this line let auth_service = AuthServiceImpl { db_pool: db_pool.clone() };
Server::builder() Server::builder()
.add_service(AdresarServer::new(AdresarService { db_pool: db_pool.clone() })) .add_service(AdresarServer::new(AdresarService { db_pool: db_pool.clone() }))

View File

@@ -0,0 +1,36 @@
// src/server/services/auth_service.rs
use tonic::{Request, Response, Status};
use common::proto::multieko2::auth::{
auth_service_server::AuthService,
RegisterRequest, AuthResponse,
LoginRequest, LoginResponse
};
use crate::auth::handlers::{
login::login,
register::register
};
use sqlx::PgPool;
#[derive(Debug)]
pub struct AuthServiceImpl {
pub db_pool: PgPool,
}
#[tonic::async_trait]
impl AuthService for AuthServiceImpl {
async fn register(
&self,
request: Request<RegisterRequest>,
) -> Result<Response<AuthResponse>, Status> {
let response = register(&self.db_pool, request.into_inner()).await?;
Ok(response)
}
async fn login(
&self,
request: Request<LoginRequest>,
) -> Result<Response<LoginResponse>, Status> {
let response = login(&self.db_pool, request.into_inner()).await?;
Ok(response)
}
}

View File

@@ -6,6 +6,7 @@ pub mod uctovnictvo_service;
pub mod table_definition_service; pub mod table_definition_service;
pub mod tables_data_service; pub mod tables_data_service;
pub mod table_script_service; pub mod table_script_service;
pub mod auth_service;
pub use adresar_service::AdresarService; pub use adresar_service::AdresarService;
pub use table_structure_service::TableStructureHandler; pub use table_structure_service::TableStructureHandler;
@@ -13,3 +14,4 @@ pub use uctovnictvo_service::UctovnictvoService;
pub use table_definition_service::TableDefinitionService; pub use table_definition_service::TableDefinitionService;
pub use tables_data_service::TablesDataService; pub use tables_data_service::TablesDataService;
pub use table_script_service::TableScriptService; pub use table_script_service::TableScriptService;
pub use auth_service::AuthServiceImpl;