From f6538945c57bc93f57c82ddc99799991eab22406 Mon Sep 17 00:00:00 2001 From: Anton Bilous Date: Wed, 12 Feb 2025 09:04:19 +0200 Subject: [PATCH] Detach authentication --- src/README.md | 5 +- src/assets/init/init.sql | 4 +- src/data/src/adapter/mysql/search.rs | 10 +-- src/data/src/lib.rs | 3 +- src/data/src/port/search.rs | 2 +- src/service/src/authentication/contract.rs | 2 +- src/service/src/lib.rs | 4 +- src/service/src/search.rs | 9 +++ src/service/src/search/adapter.rs | 43 +++++++++++ src/service/src/search/contract.rs | 60 ++++++++++++++++ src/service/src/search/repository.rs | 6 ++ src/service/src/search/service.rs | 27 +++++++ src/src/authentication.rs | 83 ++++++++++++++++++++++ src/src/{ => authentication}/login.rs | 7 +- src/src/{ => authentication}/register.rs | 22 ++++-- src/src/main.rs | 57 ++++----------- 16 files changed, 278 insertions(+), 66 deletions(-) create mode 100644 src/service/src/search.rs create mode 100644 src/service/src/search/adapter.rs create mode 100644 src/service/src/search/contract.rs create mode 100644 src/service/src/search/repository.rs create mode 100644 src/service/src/search/service.rs create mode 100644 src/src/authentication.rs rename src/src/{ => authentication}/login.rs (96%) rename src/src/{ => authentication}/register.rs (91%) diff --git a/src/README.md b/src/README.md index d393cad..8376556 100644 --- a/src/README.md +++ b/src/README.md @@ -1,4 +1,4 @@ -# Stuff that helped: +# Stuff that helped * Architecture: - [How to apply hexagonal architecture to Rust](https://www.barrage.net/blog/technology/how-to-apply-hexagonal-architecture-to-rust) @@ -11,9 +11,10 @@ - [Building a simple text editor with iced, a cross-platform GUI library for Rust](https://www.youtube.com/watch?v=gcBJ7cPSALo) - [Unofficial Iced Guide](https://jl710.github.io/iced-guide/) - [icebreaker](https://github.com/hecrj/icebreaker) + - [Halloy](https://github.com/squidowl/halloy) --- > _The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise._ -— Edsger W. Dijkstra +— _Edsger W. Dijkstra_ diff --git a/src/assets/init/init.sql b/src/assets/init/init.sql index 6d1981a..73da14c 100644 --- a/src/assets/init/init.sql +++ b/src/assets/init/init.sql @@ -22,7 +22,7 @@ CREATE TABLE PackageBases ( description VARCHAR(510) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- User roles for working on packages: flagger, packager, submitter, maintainer, etc. @@ -62,7 +62,7 @@ CREATE TABLE Packages ( flagged_at TIMESTAMP NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (base) REFERENCES PackageBases (id) ON DELETE CASCADE ); diff --git a/src/data/src/adapter/mysql/search.rs b/src/data/src/adapter/mysql/search.rs index 684cbf0..0dbc964 100644 --- a/src/data/src/adapter/mysql/search.rs +++ b/src/data/src/adapter/mysql/search.rs @@ -1,13 +1,13 @@ +use crate::Result; use crate::port::search::{Data, Entry, Mode, Order, SearchRepository}; -use crate::{Result, adapter::mysql::search}; // use chrono::Utc; use futures::TryStreamExt; use sqlx::{Executor, MySql, QueryBuilder, Row}; -pub struct UserAdapter; +pub struct SearchAdapter; -impl SearchRepository for UserAdapter +impl SearchRepository for SearchAdapter where E: Send, for<'a> &'a E: Executor<'a, Database = MySql>, @@ -139,13 +139,13 @@ mod tests { let data = Data { mode: Mode::NameAndDescription, order: Order::UpdatedAt, - search: Search::new("f")?, + search: Search::new("f").map_err(|e| e.1)?, limit: 50, exact: true, ascending: false, }; - UserAdapter::search(&pool, data).await?; + SearchAdapter::search(&pool, data).await?; Ok(()) } diff --git a/src/data/src/lib.rs b/src/data/src/lib.rs index ac3eebb..0713e21 100644 --- a/src/data/src/lib.rs +++ b/src/data/src/lib.rs @@ -13,10 +13,11 @@ pub use chrono::Utc; pub use adapter::mysql::base::BaseAdapter as MySqlBaseAdapter; pub use adapter::mysql::package::PackageAdapter as MySqlPackageAdapter; pub use adapter::mysql::user::UserAdapter as MySqlUserAdapter; +pub use adapter::mysql::search::SearchAdapter as MySqlSearchAdapter; pub use atomic::Atomic; pub use connect::*; pub use port::base::{Base, BaseRepository}; pub use port::package::{Package, PackageRepository}; +pub use port::search::{Search, SearchRepository}; pub use port::user::{User, UserRepository}; pub use port::*; - diff --git a/src/data/src/port/search.rs b/src/data/src/port/search.rs index 0475992..8a8f33a 100644 --- a/src/data/src/port/search.rs +++ b/src/data/src/port/search.rs @@ -23,7 +23,7 @@ pub struct Data { pub order: Order, pub search: Search, - pub limit: u8, + pub limit: u16, pub exact: bool, pub ascending: bool, } diff --git a/src/service/src/authentication/contract.rs b/src/service/src/authentication/contract.rs index 0d80243..42b3d06 100644 --- a/src/service/src/authentication/contract.rs +++ b/src/service/src/authentication/contract.rs @@ -102,7 +102,7 @@ impl TryFrom for Name { fn try_from(value: String) -> Result { #[derive(Validate)] #[garde(transparent)] - struct Username<'a>(#[garde(ascii, length(chars, min = 2, max = 31))] &'a str); + struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str); match Username(value.as_str()).validate() { Ok(()) => (), diff --git a/src/service/src/lib.rs b/src/service/src/lib.rs index 9111453..60c03cb 100644 --- a/src/service/src/lib.rs +++ b/src/service/src/lib.rs @@ -1,8 +1,8 @@ pub mod authentication; +pub mod search; pub use authentication::{ Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository, AuthenticationService, }; - -// pub +pub use search::{Search, SearchAdapter, SearchContract, SearchRepository, SearchService}; diff --git a/src/service/src/search.rs b/src/service/src/search.rs new file mode 100644 index 0000000..142e065 --- /dev/null +++ b/src/service/src/search.rs @@ -0,0 +1,9 @@ +pub mod adapter; +pub mod contract; +pub mod repository; +pub mod service; + +pub use adapter::*; +pub use contract::*; +pub use repository::*; +pub use service::*; diff --git a/src/service/src/search/adapter.rs b/src/service/src/search/adapter.rs new file mode 100644 index 0000000..faa766c --- /dev/null +++ b/src/service/src/search/adapter.rs @@ -0,0 +1,43 @@ +use data::search::*; +use data::{Connect, Result}; + +use std::marker::PhantomData; + +pub struct SearchAdapter +where + C: Send, + D: Connect + Sync, + UR: SearchRepository + Sync, +{ + driver: D, + _search_repository: PhantomData, +} + +impl SearchAdapter +where + C: Send, + D: Connect + Sync, + UR: SearchRepository + Sync, +{ + pub const fn new(driver: D) -> Self { + Self { + driver, + _search_repository: PhantomData, + } + } +} + +impl super::SearchRepository for SearchAdapter +where + C: Send, //+ Sync, + D: Connect + Sync, + SR: SearchRepository + Sync, +{ + async fn search(&self, data: Data) -> Result> { + let c = self.driver.open_connection().await?; + let result = SR::search(&c, data).await?; + D::close_connection(c).await?; + + Ok(result) + } +} diff --git a/src/service/src/search/contract.rs b/src/service/src/search/contract.rs new file mode 100644 index 0000000..305b5e3 --- /dev/null +++ b/src/service/src/search/contract.rs @@ -0,0 +1,60 @@ +use data::{BoxDynError, search}; +pub use data::{ + Result, Validation, + search::{Mode, Order, Entry}, +}; + +use derive_more::{Deref, Into}; +use garde::Validate; + +pub trait SearchContract: Send { + fn search(&self, data: Data) -> impl Future>> + Send; +} + +pub struct Data { + pub mode: Mode, + pub order: Order, + pub search: Search, + + pub limit: u16, + pub exact: bool, + pub ascending: bool, +} + +impl From for search::Data { + fn from(value: Data) -> Self { + Self { + mode: value.mode, + order: value.order, + search: value.search.into(), + limit: value.limit, + exact: value.exact, + ascending: value.ascending, + } + } +} + +pub type ReturnError = (T, BoxDynError); + +#[derive(Clone, Deref, Into)] +pub struct Search(search::Search); +impl AsRef for Search { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl TryFrom for Search { + type Error = ReturnError; + + fn try_from(value: String) -> Result { + #[derive(Validate)] + #[garde(transparent)] + struct Check<'a>(#[garde(ascii, length(chars, min = 1, max = 255))] &'a str); + + match Check(value.as_str()).validate() { + Ok(()) => (), + Err(e) => return Err((value, e.into())), + } + Ok(Self(search::Search::new(value)?)) + } +} diff --git a/src/service/src/search/repository.rs b/src/service/src/search/repository.rs new file mode 100644 index 0000000..0626718 --- /dev/null +++ b/src/service/src/search/repository.rs @@ -0,0 +1,6 @@ +use data::Result; +use data::search::{Data, Entry}; + +pub trait SearchRepository { + fn search(&self, data: Data) -> impl Future>> + Send; +} diff --git a/src/service/src/search/service.rs b/src/service/src/search/service.rs new file mode 100644 index 0000000..48a9e90 --- /dev/null +++ b/src/service/src/search/service.rs @@ -0,0 +1,27 @@ +use super::{Data, Result, SearchContract, SearchRepository}; +use data::search; + +pub struct SearchService +where + R: SearchRepository, +{ + pub(crate) repository: R, +} + +impl SearchService +where + R: SearchRepository, +{ + pub const fn new(repository: R) -> Self { + Self { repository } + } +} + +impl SearchContract for SearchService +where + R: SearchRepository + Send + Sync, +{ + async fn search(&self, data: Data) -> Result> { + self.repository.search(data.into()).await + } +} diff --git a/src/src/authentication.rs b/src/src/authentication.rs new file mode 100644 index 0000000..433bc8e --- /dev/null +++ b/src/src/authentication.rs @@ -0,0 +1,83 @@ +mod login; +mod register; + +use login::Login; +use register::Register; + +use service::{Authenticated, AuthenticationContract}; + +use iced::{Element, Task, futures::lock::Mutex}; +use std::sync::Arc; + +pub struct Authentication { + login: Login, + register: Register, + screen: Screen, +} + +enum Screen { + Login, + Register, +} + +#[derive(Debug)] +pub enum Message { + Login(login::Message), + Register(register::Message), +} + +pub enum Event { + Task(Task), + Authenticated(Authenticated), +} +impl From> for Event { + fn from(value: Task) -> Self { + Self::Task(value) + } +} + +impl Authentication { + pub fn new(service: Arc>) -> Self { + Self { + login: Login::new(service.clone()), + register: Register::new(service), + screen: Screen::Login, + } + } + + pub fn update(&mut self, message: Message) -> Option { + Some(match message { + Message::Login(message) => match self.login.update(message)? { + login::Event::SwitchToRegister => { + self.screen = Screen::Register; + return None; + } + + login::Event::Task(task) => task.map(Message::Login).into(), + login::Event::Authenticated(x) => Event::Authenticated(x), + }, + Message::Register(message) => match self.register.update(message)? { + register::Event::SwitchToLogin => { + self.screen = Screen::Login; + return None; + } + register::Event::Task(task) => task.map(Message::Register).into(), + register::Event::Authenticated(x) => Event::Authenticated(x), + }, + }) + } + + pub fn view(&self) -> Element { + match self.screen { + Screen::Login => self.login.view().map(Message::Login), + Screen::Register => self.register.view().map(Message::Register), + } + } + + pub fn title(&self) -> String { + match self.screen { + Screen::Login => self.login.title(), + Screen::Register => self.register.title(), + } + } +} diff --git a/src/src/login.rs b/src/src/authentication/login.rs similarity index 96% rename from src/src/login.rs rename to src/src/authentication/login.rs index 880db49..e5990fc 100644 --- a/src/src/login.rs +++ b/src/src/authentication/login.rs @@ -22,6 +22,7 @@ pub struct Login { enum State { None, Requesting, + Success, Error(String), } @@ -104,7 +105,10 @@ impl Login { ); } Message::RequestResult(r) => match &*r { - Ok(a) => return Some(Event::Authenticated(a.clone())), + Ok(a) => { + self.state = State::Success; + return Some(Event::Authenticated(a.clone())); + } Err(e) => { self.state = State::None; @@ -170,6 +174,7 @@ impl Login { match &self.state { State::None => error.map_or_else(|| "Login".into(), Into::into), + State::Success => "Success".into(), State::Requesting => "Requesting...".into(), State::Error(e) => e.into(), } diff --git a/src/src/register.rs b/src/src/authentication/register.rs similarity index 91% rename from src/src/register.rs rename to src/src/authentication/register.rs index e62876c..b1a78e4 100644 --- a/src/src/register.rs +++ b/src/src/authentication/register.rs @@ -1,4 +1,4 @@ -use crate::input::Input; +use crate::input::{self, Input, Value}; use crate::widget::centerbox; use service::authentication::{self, Email, Name, Password, RegisterData}; use service::{ @@ -15,7 +15,7 @@ pub struct Register { name: Input, email: Input, password: Input, - repeat: Input, + repeat: Input, show_password: bool, state: State, @@ -23,6 +23,7 @@ pub struct Register { } enum State { None, + Success, Requesting, Error(String), } @@ -72,7 +73,10 @@ impl Register { } fn check_passwords(&mut self) { - if self.password.as_ref() != self.repeat.as_ref() { + if self.password.as_ref() == self.repeat.as_ref() { + self.repeat + .set_value(Value::Valid(self.repeat.as_ref().to_string())); + } else { self.repeat.set_error(&"passwords are different"); } } @@ -86,7 +90,7 @@ impl Register { self.check_passwords(); } Message::RepeatChanged(s) => { - self.repeat.update(s); + self.repeat.set_value(Value::Valid(s)); self.check_passwords(); } Message::ShowPasswordToggled(b) => self.show_password = b, @@ -97,10 +101,10 @@ impl Register { Message::EmailSubmitted => return Some(self.password.focus().into()), Message::PasswordSubmitted if self.password.critical() => (), Message::PasswordSubmitted => return Some(self.repeat.focus().into()), - Message::RepeatSubmitted if self.repeat.critical() => (), + Message::RepeatSubmitted if self.repeat.error().is_some() => (), Message::RegisterPressed | Message::RepeatSubmitted => { - if self.repeat.critical() { + if self.repeat.error().is_some() { return Some(self.repeat.focus().into()); } @@ -140,7 +144,10 @@ impl Register { Message::LoginPressed => return Some(Event::SwitchToLogin), Message::RequestResult(r) => match &*r { - Ok(a) => return Some(Event::Authenticated(a.clone())), + Ok(a) => { + self.state = State::Success; + return Some(Event::Authenticated(a.clone())) + } Err(e) => { self.state = State::None; @@ -220,6 +227,7 @@ impl Register { match &self.state { State::None => error.map_or_else(|| "Register".into(), Into::into), + State::Success => "Success".into(), State::Requesting => "Requesting...".into(), State::Error(e) => e.into(), } diff --git a/src/src/main.rs b/src/src/main.rs index 7355706..407fc40 100644 --- a/src/src/main.rs +++ b/src/src/main.rs @@ -1,15 +1,12 @@ // mod main_window; // mod authentication; +mod authentication; mod input; -mod login; -mod register; mod widget; use std::sync::Arc; -use crate::login::Login; -use crate::register::Register; -// use crate::authentication::Authentication; +use crate::authentication::Authentication; // use crate::main_window::MainWindow; use data::{MySqlPool, MySqlUserAdapter, SqlxPool}; @@ -40,18 +37,9 @@ fn main() -> iced::Result { struct Repository { scale_factor: f64, main_id: window::Id, - login: - Login>>, - register: Register< + authentication: Authentication< AuthenticationService>, >, - screen: Screen, - // authentication: Authentication, -} - -enum Screen { - Login, - Register, } #[derive(Debug)] @@ -61,8 +49,7 @@ enum Message { WindowOpened(window::Id), WindowClosed(window::Id), - Login(login::Message), - Register(register::Message), + Authentecation(authentication::Message), // MainWindow(main_window::Message), } @@ -87,9 +74,7 @@ impl Repository { Self { scale_factor: 1.4, main_id, - login: Login::new(auth_service.clone()), - register: Register::new(auth_service), - screen: Screen::Login, + authentication: Authentication::new(auth_service), }, Task::batch([ open_task.map(Message::WindowOpened), @@ -112,24 +97,14 @@ impl Repository { return iced::exit(); } } - Message::Login(message) => { - if let Some(action) = self.login.update(message) { + Message::Authentecation(message) => { + if let Some(action) = self.authentication.update(message) { match action { - login::Event::SwitchToRegister => self.screen = Screen::Register, - login::Event::Task(task) => return task.map(Message::Login), - login::Event::Authenticated(authenticated) => { - log!("authenticated via login {:#?}", authenticated); + authentication::Event::Task(task) => { + return task.map(Message::Authentecation); } - } - } - } - Message::Register(message) => { - if let Some(action) = self.register.update(message) { - match action { - register::Event::SwitchToLogin => self.screen = Screen::Login, - register::Event::Task(task) => return task.map(Message::Register), - register::Event::Authenticated(authenticated) => { - log!("authenticated via register: {:#?}", authenticated); + authentication::Event::Authenticated(authenticated) => { + log!("authenticated via login {:#?}", authenticated); } } } @@ -145,10 +120,7 @@ impl Repository { fn view(&self, id: window::Id) -> Element { if self.main_id == id { // self.main_window.view().map(Message::MainWindow) - match self.screen { - Screen::Login => self.login.view().map(Message::Login), - Screen::Register => self.register.view().map(Message::Register), - } + self.authentication.view().map(Message::Authentecation) } else { center(row!["This window is unknown.", "It may be closed."]).into() } @@ -156,10 +128,7 @@ impl Repository { fn title(&self, _: window::Id) -> String { // "Repository".into() - match self.screen { - Screen::Login => self.login.title(), - Screen::Register => self.register.title(), - } + self.authentication.title() } fn subscription(&self) -> Subscription {