diff --git a/3/coursework/src/.gitignore b/3/coursework/src/.gitignore index edb52da..b59bb3c 100644 --- a/3/coursework/src/.gitignore +++ b/3/coursework/src/.gitignore @@ -1,3 +1,2 @@ /target/ /**/scrapyard/ -/database/data/ diff --git a/3/coursework/src/Cargo.lock b/3/coursework/src/Cargo.lock index 679dee7..8431ca4 100644 --- a/3/coursework/src/Cargo.lock +++ b/3/coursework/src/Cargo.lock @@ -326,17 +326,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "authentication" -version = "0.1.0" -dependencies = [ - "argon2", - "database", - "derive_more", - "garde", - "thiserror 2.0.11", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -911,7 +900,7 @@ dependencies = [ ] [[package]] -name = "database" +name = "data" version = "0.1.0" dependencies = [ "chrono", @@ -1791,6 +1780,7 @@ dependencies = [ "iced_core", "log", "rustc-hash 2.1.0", + "tokio", "wasm-bindgen-futures", "wasm-timer", ] @@ -2260,18 +2250,12 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -[[package]] -name = "macros" -version = "0.1.0" - [[package]] name = "main" version = "0.1.0" dependencies = [ "iced", - "macros", "strum", - "widget", ] [[package]] @@ -3464,6 +3448,17 @@ dependencies = [ "serde", ] +[[package]] +name = "service" +version = "0.1.0" +dependencies = [ + "argon2", + "data", + "derive_more", + "garde", + "thiserror 2.0.11", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4323,6 +4318,13 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "view" +version = "0.1.0" +dependencies = [ + "iced", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4692,13 +4694,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" -[[package]] -name = "widget" -version = "0.1.0" -dependencies = [ - "iced", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/3/coursework/src/Cargo.toml b/3/coursework/src/Cargo.toml index 78b5bb8..99ee3bb 100644 --- a/3/coursework/src/Cargo.toml +++ b/3/coursework/src/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["app/*", "libs/*", "database"] +members = ["data", "main", "service", "view"] diff --git a/3/coursework/src/app/authentication/src/lib.rs b/3/coursework/src/app/authentication/src/lib.rs deleted file mode 100644 index 1a8b0c9..0000000 --- a/3/coursework/src/app/authentication/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -// use database::port::user; - -mod repository; -mod service; diff --git a/3/coursework/src/app/authentication/src/service.rs b/3/coursework/src/app/authentication/src/service.rs deleted file mode 100644 index 0e7fa60..0000000 --- a/3/coursework/src/app/authentication/src/service.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::repository::{Authenticated, AuthenticationRepository}; -use database::port::user; - -use derive_more::{Deref, Into}; -use garde::Validate; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - // Login - #[error("login not found")] - LoginNotFound, - #[error("incorrect password")] - IncorrectPassword, - // Register - #[error("username is taken")] - NameExists, - #[error("email is already in use")] - EmailExists, - // Shared - #[error("invalid password: {0}")] - InvalidPassword(Box), - #[error("data source error: {0}")] - Repository(Box), -} - -pub type Result = std::result::Result; - -#[derive(Clone, Deref, Into)] -pub struct Name(user::Name); - -impl TryFrom for Name { - type Error = Box; - - fn try_from(value: String) -> std::result::Result { - #[derive(Validate)] - #[garde(transparent)] - struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str); - - Username(&value).validate()?; - Ok(Self(user::Name::try_from(value)?)) - } -} - -#[derive(Clone, Deref, Into)] -pub struct Email(user::Email); -impl TryFrom for Email { - type Error = Box; - - fn try_from(value: String) -> std::result::Result { - #[derive(Validate)] - #[garde(transparent)] - pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str); - - Email(&value).validate()?; - Ok(Self(user::Email::try_from(value)?)) - } -} - -// #[derive(Validate, Deref, From, Into)] -// #[garde(transparent)] -// pub struct Password(#[garde(length(chars, min = 8))] pub String); - -pub enum LoginBy { - Name(Name), - Email(Email), -} - -pub struct LoginData { - login: LoginBy, - password: String, -} - -pub struct RegisterData { - pub name: Name, - pub email: Email, - pub password: String, -} - -pub trait AuthenticationContract { - async fn name_available(&self, name: Name) -> Result; - async fn email_available(&self, email: Email) -> Result; - - async fn login(&mut self, data: LoginData) -> Result; - async fn register(&mut self, data: RegisterData) -> Result; -} - -// Service - -use crate::repository::Get; - -use argon2::{ - Argon2, - password_hash::{ - self, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng, - }, -}; - -impl From for Error { - fn from(error: password_hash::Error) -> Self { - match error { - password_hash::Error::Password => Self::IncorrectPassword, - _ => Self::InvalidPassword(error.into()), - } - } -} - -pub struct AuthenticationService -where - R: AuthenticationRepository, -{ - repository: R, -} - -impl AuthenticationService -where - R: AuthenticationRepository, -{ - pub const fn new(repository: R) -> Self { - Self { repository } - } -} - -impl AuthenticationContract for AuthenticationService -where - R: AuthenticationRepository, -{ - async fn name_available(&self, name: Name) -> Result { - if self - .repository - .get_user(Get::Name(name.0)) - .await - .map_err(Error::Repository)? - .is_some() - { - return Err(Error::NameExists); - }; - Ok(()) - } - async fn email_available(&self, email: Email) -> Result { - if self - .repository - .get_user(Get::Email(email.0)) - .await - .map_err(Error::Repository)? - .is_some() - { - return Err(Error::EmailExists); - }; - Ok(()) - } - - async fn login(&mut self, data: LoginData) -> Result { - if data.password.chars().count() < 8 { - return Err(Error::InvalidPassword( - "password must be longer than 8 characters".into(), - )); - } - - let user = match data.login { - LoginBy::Name(name) => self.repository.get_user(Get::Name(name.0)), - LoginBy::Email(email) => self.repository.get_user(Get::Email(email.0)), - } - .await - .map_err(Error::Repository)? - .ok_or(Error::LoginNotFound)?; - - Argon2::default().verify_password( - data.password.as_bytes(), - &PasswordHash::new(user.password())?, - )?; - - self.repository - .start_session(user) - .await - .map_err(Error::Repository) - } - - async fn register(&mut self, data: RegisterData) -> Result { - if data.password.chars().count() < 8 { - return Err(Error::InvalidPassword( - "password must be longer than 8 characters".into(), - )); - } - - self.name_available(data.name.clone()).await?; - self.email_available(data.email.clone()).await?; - - // Get PHC string ($argon2id$v=19$...) - let password = Argon2::default() - .hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng))? - .to_string() - .try_into() - .map_err(|e| Error::InvalidPassword(Box::from(e)))?; - - let user = self - .repository - .create_user(user::New { - name: data.name.0, - email: data.email.0, - password, - last_used: None, - }) - .await - .map_err(Error::Repository)?; - - self.repository - .start_session(user) - .await - .map_err(Error::Repository) - } -} - diff --git a/3/coursework/src/database/compose.yaml b/3/coursework/src/assets/compose.yaml similarity index 84% rename from 3/coursework/src/database/compose.yaml rename to 3/coursework/src/assets/compose.yaml index 5c2cec9..3faafd3 100644 --- a/3/coursework/src/database/compose.yaml +++ b/3/coursework/src/assets/compose.yaml @@ -5,7 +5,7 @@ services: ports: - "127.0.0.1:3306:3306" volumes: - - ./data:/var/lib/mysql + - ../../repo-database:/var/lib/mysql - ./init:/docker-entrypoint-initdb.d/:ro environment: MYSQL_ROOT_PASSWORD: password # yes, I know diff --git a/3/coursework/src/database/init/init.sql b/3/coursework/src/assets/init/init.sql similarity index 100% rename from 3/coursework/src/database/init/init.sql rename to 3/coursework/src/assets/init/init.sql diff --git a/3/coursework/src/database/.sqlx/query-014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1.json b/3/coursework/src/data/.sqlx/query-014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1.json rename to 3/coursework/src/data/.sqlx/query-014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1.json diff --git a/3/coursework/src/database/.sqlx/query-063059de083c42956506d991bc04472929e6b2618ab13eb90e772ad9bd9c1984.json b/3/coursework/src/data/.sqlx/query-063059de083c42956506d991bc04472929e6b2618ab13eb90e772ad9bd9c1984.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-063059de083c42956506d991bc04472929e6b2618ab13eb90e772ad9bd9c1984.json rename to 3/coursework/src/data/.sqlx/query-063059de083c42956506d991bc04472929e6b2618ab13eb90e772ad9bd9c1984.json diff --git a/3/coursework/src/database/.sqlx/query-0af939868e37bad5eb9097badeaefca62c247c4b2265a9667c4b33885126c771.json b/3/coursework/src/data/.sqlx/query-0af939868e37bad5eb9097badeaefca62c247c4b2265a9667c4b33885126c771.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-0af939868e37bad5eb9097badeaefca62c247c4b2265a9667c4b33885126c771.json rename to 3/coursework/src/data/.sqlx/query-0af939868e37bad5eb9097badeaefca62c247c4b2265a9667c4b33885126c771.json diff --git a/3/coursework/src/database/.sqlx/query-0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60.json b/3/coursework/src/data/.sqlx/query-0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60.json rename to 3/coursework/src/data/.sqlx/query-0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60.json diff --git a/3/coursework/src/database/.sqlx/query-346beb83d6351740a503b72133a190ac327ae79f6e555def8fec89fcc75fb015.json b/3/coursework/src/data/.sqlx/query-346beb83d6351740a503b72133a190ac327ae79f6e555def8fec89fcc75fb015.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-346beb83d6351740a503b72133a190ac327ae79f6e555def8fec89fcc75fb015.json rename to 3/coursework/src/data/.sqlx/query-346beb83d6351740a503b72133a190ac327ae79f6e555def8fec89fcc75fb015.json diff --git a/3/coursework/src/database/.sqlx/query-389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8.json b/3/coursework/src/data/.sqlx/query-389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8.json rename to 3/coursework/src/data/.sqlx/query-389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8.json diff --git a/3/coursework/src/database/.sqlx/query-404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e.json b/3/coursework/src/data/.sqlx/query-404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e.json rename to 3/coursework/src/data/.sqlx/query-404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e.json diff --git a/3/coursework/src/database/.sqlx/query-68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0.json b/3/coursework/src/data/.sqlx/query-68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0.json rename to 3/coursework/src/data/.sqlx/query-68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0.json diff --git a/3/coursework/src/database/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json b/3/coursework/src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json rename to 3/coursework/src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json diff --git a/3/coursework/src/database/.sqlx/query-7cc4cf73572c0830d1da7b8e621a79a09f3e3d8cfd42d3946bd1fac93838b913.json b/3/coursework/src/data/.sqlx/query-7cc4cf73572c0830d1da7b8e621a79a09f3e3d8cfd42d3946bd1fac93838b913.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-7cc4cf73572c0830d1da7b8e621a79a09f3e3d8cfd42d3946bd1fac93838b913.json rename to 3/coursework/src/data/.sqlx/query-7cc4cf73572c0830d1da7b8e621a79a09f3e3d8cfd42d3946bd1fac93838b913.json diff --git a/3/coursework/src/database/.sqlx/query-7f06016e9892486c938a5e94c9e5f70903a38ed314235712c28ac5e14d9ac20f.json b/3/coursework/src/data/.sqlx/query-7f06016e9892486c938a5e94c9e5f70903a38ed314235712c28ac5e14d9ac20f.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-7f06016e9892486c938a5e94c9e5f70903a38ed314235712c28ac5e14d9ac20f.json rename to 3/coursework/src/data/.sqlx/query-7f06016e9892486c938a5e94c9e5f70903a38ed314235712c28ac5e14d9ac20f.json diff --git a/3/coursework/src/database/.sqlx/query-839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc.json b/3/coursework/src/data/.sqlx/query-839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc.json rename to 3/coursework/src/data/.sqlx/query-839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc.json diff --git a/3/coursework/src/database/.sqlx/query-8af7a0169e934cb82997a1cab04e921f719ed9466c13713fda8736c540d0fa78.json b/3/coursework/src/data/.sqlx/query-8af7a0169e934cb82997a1cab04e921f719ed9466c13713fda8736c540d0fa78.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-8af7a0169e934cb82997a1cab04e921f719ed9466c13713fda8736c540d0fa78.json rename to 3/coursework/src/data/.sqlx/query-8af7a0169e934cb82997a1cab04e921f719ed9466c13713fda8736c540d0fa78.json diff --git a/3/coursework/src/database/.sqlx/query-8be76176b46f645095dce3bcbed11134ec0f43504d3a820698282848fd67dbad.json b/3/coursework/src/data/.sqlx/query-8be76176b46f645095dce3bcbed11134ec0f43504d3a820698282848fd67dbad.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-8be76176b46f645095dce3bcbed11134ec0f43504d3a820698282848fd67dbad.json rename to 3/coursework/src/data/.sqlx/query-8be76176b46f645095dce3bcbed11134ec0f43504d3a820698282848fd67dbad.json diff --git a/3/coursework/src/database/.sqlx/query-8bfaca937858ed1060da5a650f749849d29af3d6345d0e02474abf4d8c78b89d.json b/3/coursework/src/data/.sqlx/query-8bfaca937858ed1060da5a650f749849d29af3d6345d0e02474abf4d8c78b89d.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-8bfaca937858ed1060da5a650f749849d29af3d6345d0e02474abf4d8c78b89d.json rename to 3/coursework/src/data/.sqlx/query-8bfaca937858ed1060da5a650f749849d29af3d6345d0e02474abf4d8c78b89d.json diff --git a/3/coursework/src/database/.sqlx/query-8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2.json b/3/coursework/src/data/.sqlx/query-8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2.json rename to 3/coursework/src/data/.sqlx/query-8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2.json diff --git a/3/coursework/src/database/.sqlx/query-93ec7d124c9bfa7329478d975614db874788ed297fe2d95275592becb186f942.json b/3/coursework/src/data/.sqlx/query-93ec7d124c9bfa7329478d975614db874788ed297fe2d95275592becb186f942.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-93ec7d124c9bfa7329478d975614db874788ed297fe2d95275592becb186f942.json rename to 3/coursework/src/data/.sqlx/query-93ec7d124c9bfa7329478d975614db874788ed297fe2d95275592becb186f942.json diff --git a/3/coursework/src/database/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json b/3/coursework/src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json rename to 3/coursework/src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json diff --git a/3/coursework/src/database/.sqlx/query-9be7f66630e64787e55946dff428d28035747b66e57260bc9cd4634a71a037a6.json b/3/coursework/src/data/.sqlx/query-9be7f66630e64787e55946dff428d28035747b66e57260bc9cd4634a71a037a6.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-9be7f66630e64787e55946dff428d28035747b66e57260bc9cd4634a71a037a6.json rename to 3/coursework/src/data/.sqlx/query-9be7f66630e64787e55946dff428d28035747b66e57260bc9cd4634a71a037a6.json diff --git a/3/coursework/src/database/.sqlx/query-9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce.json b/3/coursework/src/data/.sqlx/query-9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce.json rename to 3/coursework/src/data/.sqlx/query-9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce.json diff --git a/3/coursework/src/database/.sqlx/query-b5814b93236d587957a103e61726b0b9ae811ba6bff0617871e76de3ef0ff662.json b/3/coursework/src/data/.sqlx/query-b5814b93236d587957a103e61726b0b9ae811ba6bff0617871e76de3ef0ff662.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-b5814b93236d587957a103e61726b0b9ae811ba6bff0617871e76de3ef0ff662.json rename to 3/coursework/src/data/.sqlx/query-b5814b93236d587957a103e61726b0b9ae811ba6bff0617871e76de3ef0ff662.json diff --git a/3/coursework/src/database/.sqlx/query-c1abf048d65d421717f20343bb0ef4fcd78f8571cfe2347c147124763bd17491.json b/3/coursework/src/data/.sqlx/query-c1abf048d65d421717f20343bb0ef4fcd78f8571cfe2347c147124763bd17491.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-c1abf048d65d421717f20343bb0ef4fcd78f8571cfe2347c147124763bd17491.json rename to 3/coursework/src/data/.sqlx/query-c1abf048d65d421717f20343bb0ef4fcd78f8571cfe2347c147124763bd17491.json diff --git a/3/coursework/src/database/.sqlx/query-c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06.json b/3/coursework/src/data/.sqlx/query-c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06.json rename to 3/coursework/src/data/.sqlx/query-c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06.json diff --git a/3/coursework/src/database/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json b/3/coursework/src/data/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json rename to 3/coursework/src/data/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json diff --git a/3/coursework/src/database/.sqlx/query-cc8f7e13c6aedf6aa4d6d4fc39db7aa98b84baf911e7f779641c1dc514c676cd.json b/3/coursework/src/data/.sqlx/query-cc8f7e13c6aedf6aa4d6d4fc39db7aa98b84baf911e7f779641c1dc514c676cd.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-cc8f7e13c6aedf6aa4d6d4fc39db7aa98b84baf911e7f779641c1dc514c676cd.json rename to 3/coursework/src/data/.sqlx/query-cc8f7e13c6aedf6aa4d6d4fc39db7aa98b84baf911e7f779641c1dc514c676cd.json diff --git a/3/coursework/src/database/.sqlx/query-cf79e2f6038dddd055d535d2c41dd8dccd1a4e6a763963590c904c25abf33137.json b/3/coursework/src/data/.sqlx/query-cf79e2f6038dddd055d535d2c41dd8dccd1a4e6a763963590c904c25abf33137.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-cf79e2f6038dddd055d535d2c41dd8dccd1a4e6a763963590c904c25abf33137.json rename to 3/coursework/src/data/.sqlx/query-cf79e2f6038dddd055d535d2c41dd8dccd1a4e6a763963590c904c25abf33137.json diff --git a/3/coursework/src/database/.sqlx/query-d289747c7c7fba86e2b66174e2d1546f10e8213d36b6b3cd25016f829e9d731b.json b/3/coursework/src/data/.sqlx/query-d289747c7c7fba86e2b66174e2d1546f10e8213d36b6b3cd25016f829e9d731b.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-d289747c7c7fba86e2b66174e2d1546f10e8213d36b6b3cd25016f829e9d731b.json rename to 3/coursework/src/data/.sqlx/query-d289747c7c7fba86e2b66174e2d1546f10e8213d36b6b3cd25016f829e9d731b.json diff --git a/3/coursework/src/database/.sqlx/query-d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661.json b/3/coursework/src/data/.sqlx/query-d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661.json rename to 3/coursework/src/data/.sqlx/query-d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661.json diff --git a/3/coursework/src/database/.sqlx/query-daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1.json b/3/coursework/src/data/.sqlx/query-daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1.json rename to 3/coursework/src/data/.sqlx/query-daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1.json diff --git a/3/coursework/src/database/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json b/3/coursework/src/data/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json rename to 3/coursework/src/data/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json diff --git a/3/coursework/src/database/.sqlx/query-e8ee44281a87c6e7147332dd5548971cb804a1ab1edcdae8bf009ac39059c2bb.json b/3/coursework/src/data/.sqlx/query-e8ee44281a87c6e7147332dd5548971cb804a1ab1edcdae8bf009ac39059c2bb.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-e8ee44281a87c6e7147332dd5548971cb804a1ab1edcdae8bf009ac39059c2bb.json rename to 3/coursework/src/data/.sqlx/query-e8ee44281a87c6e7147332dd5548971cb804a1ab1edcdae8bf009ac39059c2bb.json diff --git a/3/coursework/src/database/.sqlx/query-f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf.json b/3/coursework/src/data/.sqlx/query-f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf.json rename to 3/coursework/src/data/.sqlx/query-f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf.json diff --git a/3/coursework/src/database/.sqlx/query-f656bd1abb82c10af4e0e21b4a04a364988f5329356282f2ae0098dbfcaec671.json b/3/coursework/src/data/.sqlx/query-f656bd1abb82c10af4e0e21b4a04a364988f5329356282f2ae0098dbfcaec671.json similarity index 100% rename from 3/coursework/src/database/.sqlx/query-f656bd1abb82c10af4e0e21b4a04a364988f5329356282f2ae0098dbfcaec671.json rename to 3/coursework/src/data/.sqlx/query-f656bd1abb82c10af4e0e21b4a04a364988f5329356282f2ae0098dbfcaec671.json diff --git a/3/coursework/src/database/Cargo.toml b/3/coursework/src/data/Cargo.toml similarity index 96% rename from 3/coursework/src/database/Cargo.toml rename to 3/coursework/src/data/Cargo.toml index 0ed9ab6..432c9e2 100644 --- a/3/coursework/src/database/Cargo.toml +++ b/3/coursework/src/data/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "database" +name = "data" version = "0.1.0" edition = "2024" diff --git a/3/coursework/src/data/src/adapter.rs b/3/coursework/src/data/src/adapter.rs new file mode 100644 index 0000000..37f698b --- /dev/null +++ b/3/coursework/src/data/src/adapter.rs @@ -0,0 +1,2 @@ +//! Specific implementations of [`crate::port`]s to plug into other parts of the application. +pub mod mysql; diff --git a/3/coursework/src/database/src/adapter/mysql.rs b/3/coursework/src/data/src/adapter/mysql.rs similarity index 67% rename from 3/coursework/src/database/src/adapter/mysql.rs rename to 3/coursework/src/data/src/adapter/mysql.rs index ec9df17..9d6db43 100644 --- a/3/coursework/src/database/src/adapter/mysql.rs +++ b/3/coursework/src/data/src/adapter/mysql.rs @@ -1,3 +1,4 @@ +//! `MySQL` adapters. pub mod base; pub mod package; pub mod user; diff --git a/3/coursework/src/database/src/adapter/mysql/base.rs b/3/coursework/src/data/src/adapter/mysql/base.rs similarity index 96% rename from 3/coursework/src/database/src/adapter/mysql/base.rs rename to 3/coursework/src/data/src/adapter/mysql/base.rs index 02d358e..6d9fa8d 100644 --- a/3/coursework/src/database/src/adapter/mysql/base.rs +++ b/3/coursework/src/data/src/adapter/mysql/base.rs @@ -1,5 +1,7 @@ -pub use crate::port::base::*; +use crate::Result; +use crate::port::base::{Base, BaseRepository, Field, New}; +use chrono::Utc; use sqlx::{Executor, MySql}; pub struct BaseAdapter; diff --git a/3/coursework/src/database/src/adapter/mysql/package.rs b/3/coursework/src/data/src/adapter/mysql/package.rs similarity index 97% rename from 3/coursework/src/database/src/adapter/mysql/package.rs rename to 3/coursework/src/data/src/adapter/mysql/package.rs index 9819f30..b1286d1 100644 --- a/3/coursework/src/database/src/adapter/mysql/package.rs +++ b/3/coursework/src/data/src/adapter/mysql/package.rs @@ -1,5 +1,7 @@ -pub use crate::port::package::*; +use crate::Result; +use crate::port::package::{Field, New, Package, PackageRepository, Unique}; +use chrono::Utc; use sqlx::{Executor, MySql}; pub struct PackageAdapter; diff --git a/3/coursework/src/database/src/adapter/mysql/user.rs b/3/coursework/src/data/src/adapter/mysql/user.rs similarity index 97% rename from 3/coursework/src/database/src/adapter/mysql/user.rs rename to 3/coursework/src/data/src/adapter/mysql/user.rs index 9c06555..ca66177 100644 --- a/3/coursework/src/database/src/adapter/mysql/user.rs +++ b/3/coursework/src/data/src/adapter/mysql/user.rs @@ -1,5 +1,7 @@ -pub use crate::port::user::*; +use crate::Result; +use crate::port::user::{Field, New, Unique, User, UserRepository}; +use chrono::Utc; use sqlx::{Executor, MySql}; pub struct UserAdapter; diff --git a/3/coursework/src/database/src/atomic.rs b/3/coursework/src/data/src/atomic.rs similarity index 79% rename from 3/coursework/src/database/src/atomic.rs rename to 3/coursework/src/data/src/atomic.rs index cfb5cfe..1141e7d 100644 --- a/3/coursework/src/database/src/atomic.rs +++ b/3/coursework/src/data/src/atomic.rs @@ -1,11 +1,15 @@ -type Result = std::result::Result>; +//! Unify transaction management for established connections. +use crate::Result; pub trait Atomic { type Transaction<'a>; fn start_transaction(&mut self) -> impl Future>> + Send; - fn abort_transaction(transaction: Self::Transaction<'_>) -> impl Future + Send; - fn commit_transaction(transaction: Self::Transaction<'_>) -> impl Future + Send; + fn abort_transaction(transaction: Self::Transaction<'_>) + -> impl Future + Send; + fn commit_transaction( + transaction: Self::Transaction<'_>, + ) -> impl Future + Send; } use sqlx::Connection; diff --git a/3/coursework/src/database/src/connect.rs b/3/coursework/src/data/src/connect.rs similarity index 60% rename from 3/coursework/src/database/src/connect.rs rename to 3/coursework/src/data/src/connect.rs index 9c4ecb5..f8e054b 100644 --- a/3/coursework/src/database/src/connect.rs +++ b/3/coursework/src/data/src/connect.rs @@ -1,4 +1,5 @@ -type Result = std::result::Result>; +//! Driver to manage a connection which is passed to adapters. +use crate::Result; pub trait Connect { type Connection; @@ -8,13 +9,20 @@ pub trait Connect { } use sqlx::Connection; +pub use sqlx::MySqlConnection as SqlxConnection; +pub use sqlx::MySqlPool as SqlxPool; #[derive(Clone)] pub struct MySqlPool { - pool: sqlx::MySqlPool, + pool: SqlxPool, +} +impl MySqlPool { + pub const fn new(pool: SqlxPool) -> Self { + Self { pool } + } } impl Connect for MySqlPool { - type Connection = sqlx::MySqlPool; + type Connection = SqlxPool; async fn open_connection(&self) -> Result { Ok(self.pool.clone()) @@ -27,14 +35,16 @@ impl Connect for MySqlPool { pub struct MySqlConnection { link: String, } - +impl MySqlConnection { + pub const fn new(link: String) -> Self { + Self { link } + } +} impl Connect for MySqlConnection { - type Connection = sqlx::MySqlConnection; + type Connection = SqlxConnection; async fn open_connection(&self) -> Result { - sqlx::MySqlConnection::connect(&self.link) - .await - .map_err(Box::from) + SqlxConnection::connect(&self.link).await.map_err(Box::from) } async fn close_connection(connection: Self::Connection) -> Result { connection.close().await?; diff --git a/3/coursework/src/data/src/lib.rs b/3/coursework/src/data/src/lib.rs new file mode 100644 index 0000000..8633dbc --- /dev/null +++ b/3/coursework/src/data/src/lib.rs @@ -0,0 +1,21 @@ +//! Data access for the application. +pub mod adapter; +pub mod atomic; +pub mod connect; +pub mod port; + +// Don't want to handle errors for dynamic mess. +pub type BoxDynError = Box; +pub type Result = std::result::Result; + +pub use chrono::Utc; + +pub use atomic::Atomic; +pub use connect::*; + +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 port::base::{self, Base, BaseRepository}; +pub use port::package::{self, Package, PackageRepository}; +pub use port::user::{self, User, UserRepository}; diff --git a/3/coursework/src/database/src/port.rs b/3/coursework/src/data/src/port.rs similarity index 67% rename from 3/coursework/src/database/src/port.rs rename to 3/coursework/src/data/src/port.rs index 5daecd5..a9b70f4 100644 --- a/3/coursework/src/database/src/port.rs +++ b/3/coursework/src/data/src/port.rs @@ -1,4 +1,11 @@ -pub type Result> = std::result::Result; +//! Low-level repository traits for unified data access. +//! +//! No data validation besides very basic one like length violation. +use crate::Result; + +pub mod base; +pub mod package; +pub mod user; pub trait CRUD { type New; @@ -9,17 +16,18 @@ pub trait CRUD { fn create( connection: &mut C, data: Self::New, - ) -> impl Future> + Send; + ) -> impl Future> + Send; fn read( connection: &C, data: Self::Unique, - ) -> impl Future>> + Send; + ) -> impl Future>> + Send; fn update( connection: &mut C, existing: &mut Self::Existing, data: Self::Update, - ) -> impl Future + Send; - fn delete(connection: &mut C, data: Self::Unique) -> impl Future + Send; + ) -> impl Future + Send; + fn delete(connection: &mut C, data: Self::Unique) + -> impl Future + Send; } trait CharLength { @@ -48,9 +56,3 @@ trait MaxLength { } } } - -// const TOO_LONG: &str = "too long"; - -pub mod base; -pub mod package; -pub mod user; diff --git a/3/coursework/src/database/src/port/base.rs b/3/coursework/src/data/src/port/base.rs similarity index 85% rename from 3/coursework/src/database/src/port/base.rs rename to 3/coursework/src/data/src/port/base.rs index c312733..d7c72fe 100644 --- a/3/coursework/src/database/src/port/base.rs +++ b/3/coursework/src/data/src/port/base.rs @@ -1,11 +1,10 @@ use super::MaxLength; -pub use super::{CRUD, Result}; -pub use chrono::{DateTime, Utc}; +use chrono::{DateTime, Utc}; use derive_more::{Deref, Into}; pub trait BaseRepository: - CRUD + super::CRUD { } @@ -34,11 +33,13 @@ impl MaxLength for Description { const MAX_LENGTH: usize = 510; } impl TryFrom> for Description { - type Error = &'static str; + type Error = (Option, &'static str); fn try_from(value: Option) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } diff --git a/3/coursework/src/database/src/port/package.rs b/3/coursework/src/data/src/port/package.rs similarity index 76% rename from 3/coursework/src/database/src/port/package.rs rename to 3/coursework/src/data/src/port/package.rs index 7d4029b..6d7862e 100644 --- a/3/coursework/src/database/src/port/package.rs +++ b/3/coursework/src/data/src/port/package.rs @@ -1,11 +1,11 @@ use super::MaxLength; -pub use super::{CRUD, Result, base::Base}; +use crate::Base; -pub use chrono::{DateTime, Utc}; +use chrono::{DateTime, Utc}; use derive_more::{Deref, Into}; pub trait PackageRepository: - CRUD + super::CRUD { } @@ -16,11 +16,13 @@ impl MaxLength for Name { const MAX_LENGTH: usize = 127; } impl TryFrom for Name { - type Error = &'static str; + type Error = (String, &'static str); fn try_from(value: String) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } @@ -31,11 +33,13 @@ impl MaxLength for Version { const MAX_LENGTH: usize = 127; } impl TryFrom for Version { - type Error = &'static str; + type Error = (String, &'static str); fn try_from(value: String) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } @@ -46,11 +50,13 @@ impl MaxLength for Description { const MAX_LENGTH: usize = 255; } impl TryFrom> for Description { - type Error = &'static str; + type Error = (Option, &'static str); fn try_from(value: Option) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } @@ -61,11 +67,13 @@ impl MaxLength for URL { const MAX_LENGTH: usize = 510; } impl TryFrom> for URL { - type Error = &'static str; + type Error = (Option, &'static str); fn try_from(value: Option) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } diff --git a/3/coursework/src/database/src/port/user.rs b/3/coursework/src/data/src/port/user.rs similarity index 76% rename from 3/coursework/src/database/src/port/user.rs rename to 3/coursework/src/data/src/port/user.rs index fb49757..359ce02 100644 --- a/3/coursework/src/database/src/port/user.rs +++ b/3/coursework/src/data/src/port/user.rs @@ -1,11 +1,10 @@ use super::MaxLength; -pub use super::{CRUD, Result}; -pub use chrono::{DateTime, Utc}; +use chrono::{DateTime, Utc}; use derive_more::{Deref, Into}; pub trait UserRepository: - CRUD + super::CRUD { } @@ -16,11 +15,13 @@ impl MaxLength for Name { const MAX_LENGTH: usize = 31; } impl TryFrom for Name { - type Error = &'static str; + type Error = (String, &'static str); fn try_from(value: String) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } @@ -31,11 +32,13 @@ impl MaxLength for Email { const MAX_LENGTH: usize = 255; } impl TryFrom for Email { - type Error = &'static str; + type Error = (String, &'static str); fn try_from(value: String) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } @@ -46,11 +49,13 @@ impl MaxLength for Password { const MAX_LENGTH: usize = 255; } impl TryFrom for Password { - type Error = &'static str; + type Error = (String, &'static str); fn try_from(value: String) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + match Self::validate(&value) { + Ok(()) => Ok(Self(value)), + Err(e) => Err((value, e)), + } } } @@ -76,6 +81,7 @@ pub struct New { pub last_used: Option>, } +#[derive(Debug)] pub struct User { pub(crate) id: u64, pub(crate) name: String, diff --git a/3/coursework/src/database/src/adapter.rs b/3/coursework/src/database/src/adapter.rs deleted file mode 100644 index 4fb2dd9..0000000 --- a/3/coursework/src/database/src/adapter.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod mysql; diff --git a/3/coursework/src/database/src/lib.rs b/3/coursework/src/database/src/lib.rs deleted file mode 100644 index b66abeb..0000000 --- a/3/coursework/src/database/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod adapter; -pub mod atomic; -pub mod connect; -pub mod port; diff --git a/3/coursework/src/app/authentication/Cargo.toml b/3/coursework/src/service/Cargo.toml similarity index 79% rename from 3/coursework/src/app/authentication/Cargo.toml rename to 3/coursework/src/service/Cargo.toml index 3fafecd..372db4c 100644 --- a/3/coursework/src/app/authentication/Cargo.toml +++ b/3/coursework/src/service/Cargo.toml @@ -1,14 +1,13 @@ [package] -name = "authentication" +name = "service" version = "0.1.0" edition = "2024" [dependencies] -argon2 = { version = "0.5.3", features = ["std"] } - thiserror = "2.0.11" +argon2 = { version = "0.5.3", features = ["std"] } garde = { version = "0.22.0", features = ["email", "url", "derive"] } derive_more = { version = "1.0.0", features = ["deref", "deref_mut", "into"] } -[dependencies.database] -path = "../../database" +[dependencies.data] +path = "../data" diff --git a/3/coursework/src/service/src/authentication.rs b/3/coursework/src/service/src/authentication.rs new file mode 100644 index 0000000..142e065 --- /dev/null +++ b/3/coursework/src/service/src/authentication.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/3/coursework/src/app/authentication/src/repository.rs b/3/coursework/src/service/src/authentication/adapter.rs similarity index 62% rename from 3/coursework/src/app/authentication/src/repository.rs rename to 3/coursework/src/service/src/authentication/adapter.rs index 766731b..76173de 100644 --- a/3/coursework/src/app/authentication/src/repository.rs +++ b/3/coursework/src/service/src/authentication/adapter.rs @@ -1,33 +1,7 @@ -pub use database::port::user::*; +use super::{Authenticated, AuthenticationRepository, Get}; +use data::user::{Field, New, User, UserRepository}; +use data::{Connect, Result}; -use derive_more::{Deref, DerefMut}; - -#[derive(Deref, DerefMut)] -pub struct Authenticated(User); - -pub trait AuthenticationRepository { - fn get_user(&self, get: Get) -> impl Future>> + Send; - fn create_user(&self, new: New) -> impl Future> + Send; - fn start_session(&self, user: User) -> impl Future> + Send; -} - -pub enum Get { - Name(Name), - Email(Email), -} - -impl From for Unique { - fn from(value: Get) -> Self { - match value { - Get::Name(s) => Self::Name(s), - Get::Email(s) => Self::Email(s), - } - } -} - -// Adapter - -use database::connect::Connect; use std::marker::PhantomData; pub struct AuthenticationAdapter @@ -78,7 +52,7 @@ where async fn start_session(&self, mut user: User) -> Result { let mut c = self.driver.open_connection().await?; - UR::update(&mut c, &mut user, Field::LastUsed(Some(Utc::now()))).await?; + UR::update(&mut c, &mut user, Field::LastUsed(Some(data::Utc::now()))).await?; D::close_connection(c).await?; Ok(Authenticated(user)) diff --git a/3/coursework/src/service/src/authentication/contract.rs b/3/coursework/src/service/src/authentication/contract.rs new file mode 100644 index 0000000..b226a68 --- /dev/null +++ b/3/coursework/src/service/src/authentication/contract.rs @@ -0,0 +1,122 @@ +use super::Authenticated; +use data::user; + +use derive_more::{Deref, Into}; +use garde::Validate; + +pub type Result = std::result::Result; + +pub trait AuthenticationContract { + fn name_available(&self, name: Name) -> impl Future + Send; + fn email_available(&self, email: Email) -> impl Future + Send; + + fn login(&self, data: LoginData) -> impl Future> + Send; + fn register( + &mut self, + data: RegisterData, + ) -> impl Future> + Send; +} + +pub struct LoginData { + pub login: Login, + pub password: Password, +} + +pub struct RegisterData { + pub name: Name, + pub email: Email, + pub password: Password, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + // Login + #[error("login was not found")] + LoginNotFound, + #[error("incorrect password")] + IncorrectPassword, + // Register + #[error("username is taken")] + NameExists, + #[error("email is already in use")] + EmailExists, + // Shared + #[error("invalid password: {0}")] + InvalidPassword(data::BoxDynError), + #[error("data source error: {0}")] + Repository(data::BoxDynError), +} + +#[derive(Clone)] +pub enum Login { + Name(Name), + Email(Email), +} +impl TryFrom for Login { + type Error = (String, &'static str); + + fn try_from(value: String) -> Result { + let value = match Email::try_from(value) { + Ok(x) => return Ok(Self::Email(x)), + Err((s, _)) => s, + }; + match Name::try_from(value) { + Ok(x) => Ok(Self::Name(x)), + Err((s, _)) => Err((s, "login is invalid")), + } + } +} + +#[derive(Clone, Deref, Into)] +pub struct Name(user::Name); +impl TryFrom for Name { + type Error = (String, Box); + + fn try_from(value: String) -> Result { + #[derive(Validate)] + #[garde(transparent)] + struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str); + + if let Err(e) = Username(&value).validate() { + return Err((value, e.into())); + } + match user::Name::try_from(value) { + Ok(x) => Ok(Self(x)), + Err((s, e)) => Err((s, e.into())), + } + } +} + +#[derive(Clone, Deref, Into)] +pub struct Email(user::Email); +impl TryFrom for Email { + type Error = (String, Box); + + fn try_from(value: String) -> Result { + #[derive(Validate)] + #[garde(transparent)] + pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str); + + if let Err(e) = Email(&value).validate() { + return Err((value, e.into())); + } + match user::Email::try_from(value) { + Ok(x) => Ok(Self(x)), + Err((s, e)) => Err((s, e.into())), + } + } +} + +#[derive(Clone, Deref, Into)] +pub struct Password(String); +impl TryFrom for Password { + type Error = (String, &'static str); + + fn try_from(value: String) -> Result { + if value.chars().count() >= 8 { + Ok(Self(value)) + } else { + Err((value, "password must be 8 characters or more")) + } + } +} diff --git a/3/coursework/src/service/src/authentication/repository.rs b/3/coursework/src/service/src/authentication/repository.rs new file mode 100644 index 0000000..aa7bca6 --- /dev/null +++ b/3/coursework/src/service/src/authentication/repository.rs @@ -0,0 +1,26 @@ +use data::Result; +use data::user::{Email, Name, New, Unique, User}; + +use derive_more::{Deref, DerefMut}; + +#[derive(Deref, DerefMut, Debug)] +pub struct Authenticated(pub(super) User); + +pub trait AuthenticationRepository { + fn get_user(&self, get: Get) -> impl Future>> + Send; + fn create_user(&self, new: New) -> impl Future> + Send; + fn start_session(&self, user: User) -> impl Future> + Send; +} + +pub enum Get { + Name(Name), + Email(Email), +} +impl From for Unique { + fn from(value: Get) -> Self { + match value { + Get::Name(s) => Self::Name(s), + Get::Email(s) => Self::Email(s), + } + } +} diff --git a/3/coursework/src/service/src/authentication/service.rs b/3/coursework/src/service/src/authentication/service.rs new file mode 100644 index 0000000..7df788d --- /dev/null +++ b/3/coursework/src/service/src/authentication/service.rs @@ -0,0 +1,114 @@ +use super::{ + Authenticated, AuthenticationContract, AuthenticationRepository, Email, Error, Get, Login, + LoginData, Name, RegisterData, Result, +}; + +use argon2::{ + Argon2, + password_hash::{ + self, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng, + }, +}; + +impl From for Error { + fn from(error: password_hash::Error) -> Self { + match error { + password_hash::Error::Password => Self::IncorrectPassword, + _ => Self::InvalidPassword(error.into()), + } + } +} + +pub struct AuthenticationService +where + R: AuthenticationRepository, +{ + pub(crate) repository: R, +} + +impl AuthenticationService +where + R: AuthenticationRepository, +{ + pub const fn new(repository: R) -> Self { + Self { repository } + } +} + +impl AuthenticationContract for AuthenticationService +where + R: AuthenticationRepository + Send + Sync, +{ + async fn name_available(&self, name: Name) -> Result { + if self + .repository + .get_user(Get::Name(name.into())) + .await + .map_err(Error::Repository)? + .is_some() + { + return Err(Error::NameExists); + }; + Ok(()) + } + async fn email_available(&self, email: Email) -> Result { + if self + .repository + .get_user(Get::Email(email.into())) + .await + .map_err(Error::Repository)? + .is_some() + { + return Err(Error::EmailExists); + }; + Ok(()) + } + + async fn login(&self, data: LoginData) -> Result { + let user = match data.login { + Login::Name(name) => self.repository.get_user(Get::Name(name.into())), + Login::Email(email) => self.repository.get_user(Get::Email(email.into())), + } + .await + .map_err(Error::Repository)? + .ok_or(Error::LoginNotFound)?; + + Argon2::default().verify_password( + data.password.as_bytes(), + &PasswordHash::new(user.password())?, + )?; + + self.repository + .start_session(user) + .await + .map_err(Error::Repository) + } + + async fn register(&mut self, data: RegisterData) -> Result { + self.name_available(data.name.clone()).await?; + self.email_available(data.email.clone()).await?; + + // Get PHC string ($argon2id$v=19$...) + let password = Argon2::default() + .hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng))? + .to_string() + .try_into() + .map_err(|(_, e)| Error::InvalidPassword(Box::from(e)))?; + + let user = self + .repository + .create_user(data::user::New { + name: data.name.into(), + email: data.email.into(), + password, + last_used: None, + }) + .await + .map_err(Error::Repository)?; + + self.repository + .start_session(user) + .await + .map_err(Error::Repository) + } +} diff --git a/3/coursework/src/service/src/lib.rs b/3/coursework/src/service/src/lib.rs new file mode 100644 index 0000000..997a1fd --- /dev/null +++ b/3/coursework/src/service/src/lib.rs @@ -0,0 +1,6 @@ +pub mod authentication; + +pub use authentication::{ + Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository, + AuthenticationService, +}; diff --git a/3/coursework/src/view/Cargo.toml b/3/coursework/src/view/Cargo.toml new file mode 100644 index 0000000..b558b15 --- /dev/null +++ b/3/coursework/src/view/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "view" +version = "0.1.0" +edition = "2024" + +[dependencies] +iced = { version = "0.13.1", features = ["lazy", "tokio"] } diff --git a/3/coursework/src/view/src/authentication.rs b/3/coursework/src/view/src/authentication.rs new file mode 100644 index 0000000..f5685a3 --- /dev/null +++ b/3/coursework/src/view/src/authentication.rs @@ -0,0 +1,104 @@ +pub mod login; +pub mod register; + +use crate::input::Validation; + +use iced::Task; + +pub struct Authentication { + screen: Screen, + login: login::Login, + register: register::Register, +} +pub enum Screen { + Login, + Register, +} + +#[derive(Debug)] +pub enum Message { + Login(login::Message), + Register(register::Message), +} + +pub enum Request { + Task(Task), + SimpleLoginValidation(login::Field), + SimpleRegisterValidation(register::Field), + Login { + login: String, + password: String, + }, + Register { + name: String, + email: String, + password: String, + }, +} + +pub enum RequestResult { + Error(String), + LoginValidation(login::Field, Validation), + RegisterValidation(register::Field, Validation), +} + +impl Default for Authentication { + fn default() -> Self { + Self::new() + } +} + +impl Authentication { + pub const fn new() -> Self { + Self { + screen: Screen::Login, + login: Login::new(), + register: Register::new(), + } + } + + pub fn update(&mut self, message: Message) -> Option { + Some(match message { + Message::Login(message) => match self.login.update(message)? { + login::Request::SwitchToRegister => { + self.screen = Screen::Register; + return None; + } + login::Request::SimpleValidation(x) => Request::SimpleLoginValidation(x), + login::Request::Task(task) => Request::Task(task.map(Message::Login)), + login::Request::Login { login, password } => Request::Login { login, password }, + }, + Message::Register(message) => match self.register.update(message)? { + register::Request::SwitchToLogin => { + self.screen = Screen::Login; + return None; + } + register::Request::SimpleValidation(x) => Request::SimpleRegisterValidation(x), + register::Request::Task(task) => Request::Task(task.map(Message::Register)), + register::Request::Register { + name, + email, + password, + } => Request::Register { + name, + email, + password, + }, + }, + }) + } + + pub fn view(&self) -> iced::Element { + match self.screen { + Screen::Login => self.login.view().map(Message::Login), + Screen::Register => self.register.view().map(Message::Register), + } + } + + pub fn title(&self) -> std::borrow::Cow { + match self.screen { + Screen::Login => self.login.title(), + Screen::Register => self.register.title(), + } + } +} diff --git a/3/coursework/src/view/src/authentication/login.rs b/3/coursework/src/view/src/authentication/login.rs new file mode 100644 index 0000000..d0f2e3b --- /dev/null +++ b/3/coursework/src/view/src/authentication/login.rs @@ -0,0 +1,164 @@ +use crate::input::{Input, Validation}; +use crate::widget::centerbox; + +use iced::widget::{Space, button, checkbox, column, container, row, text}; +use iced::{Length, Task, padding}; + +pub struct Login { + state: State, + login: Input, + password: Input, + show_password: bool, +} +enum State { + None, + Requesting, + Error(String), +} + +pub enum Request { + SwitchToRegister, + SimpleValidation(Field), + Task(Task), + Login { login: String, password: String }, +} +pub enum Field { + Login(String), + Password(String), +} + +pub enum RequestResult { + Error(String), + Validation(Field, Validation), +} + +#[derive(Debug, Clone)] +pub enum Message { + LoginChanged(String), + PasswordChanged(String), + ShowPasswordToggled(bool), + + LoginSubmitted, + PasswordSubmitted, + + LoginPressed, + RegisterPressed, +} + +impl Default for Login { + fn default() -> Self { + Self::new() + } +} + +impl Login { + pub const fn new() -> Self { + Self { + state: State::None, + login: Input::new("login_name"), + password: Input::new("login_password"), + show_password: false, + } + } + + pub fn handle_result(&mut self, result: RequestResult) { + match result { + RequestResult::Error(e) => self.state = State::Error(e), + RequestResult::Validation(field, validation) => match &field { + Field::Login(s) => self.login.apply_if_eq(s, validation), + Field::Password(s) => self.password.apply_if_eq(s, validation), + }, + } + } + + pub fn update(&mut self, message: Message) -> Option { + Some(match message { + Message::LoginChanged(s) => { + self.login.update(s.clone()); + Request::SimpleValidation(Field::Login(s)) + } + Message::PasswordChanged(s) => { + self.password.update(s.clone()); + Request::SimpleValidation(Field::Password(s)) + } + + Message::ShowPasswordToggled(b) => { + self.show_password = b; + return None; + } + + Message::LoginSubmitted if !self.login.submittable() => return None, + Message::LoginSubmitted => Request::Task(self.login.focus()), + + Message::LoginPressed | Message::PasswordSubmitted => { + if !self.login.submittable() { + Request::Task(self.login.focus()) + } else if !self.password.submittable() { + Request::Task(self.password.focus()) + } else { + self.state = State::Requesting; + + Request::Login { + login: self.login.value().into(), + password: self.password.value().into(), + } + } + } + + Message::RegisterPressed => Request::SwitchToRegister, + }) + } + + pub fn view(&self) -> iced::Element { + centerbox( + column![ + container(text(self.title()).size(20)) + .center_x(Length::Fill) + .padding(padding::bottom(10)), + self.login + .view("Email or Username") + .on_input(Message::LoginChanged) + .on_submit(Message::LoginSubmitted), + self.password + .view("Password") + .on_input(Message::PasswordChanged) + .on_submit(Message::PasswordSubmitted) + .secure(!self.show_password), + checkbox("Show password", self.show_password) + .on_toggle(Message::ShowPasswordToggled), + row![ + button(text("Register").center().size(18)) + .on_press(Message::RegisterPressed) + .style(button::secondary) + .width(Length::FillPortion(3)) + .padding(10), + Space::with_width(Length::FillPortion(2)), + button(text("Login").center().size(18)) + .on_press(Message::LoginPressed) + .style(button::primary) + .width(Length::FillPortion(3)) + .padding(10) + ] + .padding(padding::top(15)), + ] + .width(Length::Fixed(250.)) + .spacing(20), + ) + } + + pub fn title(&self) -> std::borrow::Cow { + let errors = [ + self.login.error(), + self.password.error(), + self.login.warning(), + self.password.warning(), + ]; + let error = errors.into_iter().flatten().next(); + + match &self.state { + State::None => error.map_or_else(|| "Login".into(), Into::into), + State::Requesting => "Requesting...".into(), + State::Error(e) => e.into(), + } + } +} diff --git a/3/coursework/src/view/src/authentication/register.rs b/3/coursework/src/view/src/authentication/register.rs new file mode 100644 index 0000000..a3db851 --- /dev/null +++ b/3/coursework/src/view/src/authentication/register.rs @@ -0,0 +1,216 @@ +use crate::input::{Input, Validation}; +use crate::widget::centerbox; + +use iced::widget::{Space, button, checkbox, column, container, row, text}; +use iced::{Length, Task, padding}; + +pub struct Register { + state: State, + name: Input, + email: Input, + password: Input, + repeat: Input, + show_password: bool, +} +enum State { + None, + Requesting, + Error(String), +} + +pub enum Request { + SwitchToLogin, + SimpleValidation(Field), + Task(Task), + Register { + name: String, + email: String, + password: String, + }, +} +pub enum Field { + Name(String), + Email(String), + Password(String), +} + +pub enum RequestResult { + Error(String), + Validation(Field, Validation), +} + +#[derive(Debug, Clone)] +pub enum Message { + NameChanged(String), + EmailChanged(String), + PasswordChanged(String), + RepeatChanged(String), + ShowPasswordToggled(bool), + + EmailSubmitted, + NameSubmitted, + PasswordSubmitted, + RepeatSubmitted, + + RegisterPressed, + LoginPressed, +} + +impl Default for Register { + fn default() -> Self { + Self::new() + } +} + +impl Register { + pub const fn new() -> Self { + Self { + state: State::None, + name: Input::new("register_name"), + email: Input::new("register_email"), + password: Input::new("register_password"), + repeat: Input::new("register_repeat"), + show_password: false, + } + } + + pub fn handle_result(&mut self, result: RequestResult) { + match result { + RequestResult::Error(e) => self.state = State::Error(e), + RequestResult::Validation(field, validation) => match &field { + Field::Name(s) => self.name.apply_if_eq(s, validation), + Field::Email(s) => self.email.apply_if_eq(s, validation), + Field::Password(s) => self.password.apply_if_eq(s, validation), + }, + } + } + + #[inline] + fn check_passwords(&mut self) { + if self.password.value() != self.repeat.value() { + self.repeat.set_error("passwords are different".into()); + } + } + pub fn update(&mut self, message: Message) -> Option { + Some(match message { + Message::NameChanged(s) => { + self.name.update(s.clone()); + Request::SimpleValidation(Field::Name(s)) + } + Message::EmailChanged(s) => { + self.email.update(s.clone()); + Request::SimpleValidation(Field::Email(s)) + } + Message::PasswordChanged(s) => { + self.password.update(s.clone()); + self.check_passwords(); + Request::SimpleValidation(Field::Password(s)) + } + Message::RepeatChanged(s) => { + self.repeat.update(s); + self.check_passwords(); + return None; + } + + Message::ShowPasswordToggled(b) => { + self.show_password = b; + return None; + } + + Message::NameSubmitted if !self.name.submittable() => return None, + Message::NameSubmitted => Request::Task(self.email.focus()), + Message::EmailSubmitted if !self.email.submittable() => return None, + Message::EmailSubmitted => Request::Task(self.password.focus()), + Message::PasswordSubmitted if !self.password.submittable() => return None, + Message::PasswordSubmitted => Request::Task(self.repeat.focus()), + + Message::RegisterPressed | Message::RepeatSubmitted => { + if !self.name.submittable() { + Request::Task(self.name.focus()) + } else if !self.email.submittable() { + Request::Task(self.email.focus()) + } else if !self.password.submittable() { + Request::Task(self.password.focus()) + } else if !self.repeat.submittable() { + Request::Task(self.repeat.focus()) + } else { + self.state = State::Requesting; + + Request::Register { + name: self.name.value().into(), + email: self.email.value().into(), + password: self.password.value().into(), + } + } + } + + Message::LoginPressed => Request::SwitchToLogin, + }) + } + + pub fn view(&self) -> iced::Element { + centerbox( + column![ + container(text(self.title()).size(20)) + .center_x(Length::Fill) + .padding(padding::bottom(10)), + self.name + .view("Username") + .on_input(Message::NameChanged) + .on_submit(Message::NameSubmitted), + self.email + .view("Email") + .on_input(Message::EmailChanged) + .on_submit(Message::EmailSubmitted), + self.password + .view("Password") + .on_input(Message::PasswordChanged) + .on_submit(Message::PasswordSubmitted) + .secure(!self.show_password), + self.repeat + .view("Repeat Password") + .on_input(Message::RepeatChanged) + .on_submit(Message::RepeatSubmitted) + .secure(!self.show_password), + checkbox("Show password", self.show_password) + .on_toggle(Message::ShowPasswordToggled), + row![ + button(text("Login").center().size(18)) + .on_press(Message::LoginPressed) + .style(button::secondary) + .width(Length::FillPortion(3)) + .padding(10), + Space::with_width(Length::FillPortion(2)), + button(text("Register").center().size(18)) + .on_press(Message::RegisterPressed) + .style(button::primary) + .width(Length::FillPortion(3)) + .padding(10), + ] + .padding(padding::top(15)), + ] + .width(Length::Fixed(250.)) + .spacing(20), + ) + } + + pub fn title(&self) -> std::borrow::Cow { + let errors = [ + self.name.error(), + self.email.error(), + self.password.error(), + self.repeat.error(), + self.name.warning(), + self.email.warning(), + self.password.warning(), + self.repeat.warning(), + ]; + let error = errors.into_iter().flatten().next(); + + match &self.state { + State::None => error.map_or_else(|| "Register".into(), Into::into), + State::Requesting => "Requesting...".into(), + State::Error(e) => e.into(), + } + } +} diff --git a/3/coursework/src/view/src/input.rs b/3/coursework/src/view/src/input.rs new file mode 100644 index 0000000..5e8a70a --- /dev/null +++ b/3/coursework/src/view/src/input.rs @@ -0,0 +1,93 @@ +use crate::widget::text_input::{error, success, warning}; + +use iced::widget::{TextInput, text_input, text_input::default}; + +pub struct Input { + id: &'static str, + value: String, + warning: Option, + state: State, +} +enum State { + None, + Valid, + Invalid(String), +} + +pub enum Validation { + Valid, + Warning(String), + Invalid(String), +} + +impl Input { + pub const fn new(id: &'static str) -> Self { + Self { + id, + value: String::new(), + warning: None, + state: State::None, + } + } + + pub fn value(&self) -> &str { + self.value.as_ref() + } + pub fn error(&self) -> Option<&str> { + match &self.state { + State::Invalid(e) => Some(e.as_ref()), + _ => None, + } + } + pub fn warning(&self) -> Option<&str> { + self.warning.as_ref().map(AsRef::as_ref) + } + // pub fn submit(&self) -> Result { + // match &self.state { + // State::Invalid(e) => Err(e.as_ref()), + // State::None | State::Valid => Ok(self.value.clone()), + // } + // } + pub const fn submittable(&self) -> bool { + !matches!(self.state, State::Invalid(_)) + } + + pub fn update(&mut self, value: String) { + self.value = value; + self.warning = None; + self.state = State::None; + } + pub fn set_error(&mut self, value: String) { + self.state = State::Invalid(value); + } + pub fn set_warning(&mut self, value: String) { + self.warning = Some(value); + } + pub fn apply(&mut self, validation: Validation) { + match validation { + Validation::Valid => self.state = State::Valid, + Validation::Warning(w) => self.warning = Some(w), + Validation::Invalid(e) => self.state = State::Invalid(e), + } + } + pub fn apply_if_eq(&mut self, value: &str, validation: Validation) { + if self.value == value { + self.apply(validation); + } + } + + pub fn focus(&self) -> iced::Task { + iced::widget::text_input::focus(self.id) + } + pub fn view(&self, placeholder: &str) -> TextInput { + text_input(placeholder, &self.value) + .id(self.id) + .padding(12) + .style(match self.state { + State::None if self.warning.is_none() => default, + State::Valid if self.warning.is_none() => success, + State::Invalid(_) => error, + _ => warning, + }) + } +} diff --git a/3/coursework/src/view/src/lib.rs b/3/coursework/src/view/src/lib.rs new file mode 100644 index 0000000..df79b91 --- /dev/null +++ b/3/coursework/src/view/src/lib.rs @@ -0,0 +1,7 @@ +mod widget; + +mod input; +pub use input::Validation; + +pub mod authentication; +pub use authentication::{Authentication, login, register}; diff --git a/3/coursework/src/view/src/widget.rs b/3/coursework/src/view/src/widget.rs new file mode 100644 index 0000000..e9e3f7c --- /dev/null +++ b/3/coursework/src/view/src/widget.rs @@ -0,0 +1,71 @@ +use iced::widget::{Scrollable, center, container, mouse_area, scrollable, text, tooltip}; +use iced::{Element, color}; + +/// Put content into a dark container at the center of the screen +/// which can be scrolled in multiple dirrections +pub fn centerbox<'a, Message: 'a>( + content: impl Into>, +) -> Element<'a, Message> { + center(scroll( + container(content).style(container::dark).padding(20), + )) + .into() +} + +/// Scrollable but in both vertical and horizontal directions +pub fn scroll<'a, Message: 'a>(content: impl Into>) -> Element<'a, Message> { + Scrollable::with_direction(content, scrollable::Direction::Both { + vertical: scrollable::Scrollbar::default(), + horizontal: scrollable::Scrollbar::default(), + }) + .into() +} + +/// Clickable url +pub fn url<'a, Message: Clone + 'a>(txt: &impl ToString, msg: Message) -> Element<'a, Message> { + Element::from(mouse_area(text(txt.to_string()).color(color!(0xBB_B6_DF))).on_press(msg)) +} + +pub mod tip { + pub use iced::widget::tooltip::Position; +} + +/// Tooltip with some styling applied +pub fn tip<'a, Message: 'a>( + content: impl Into>, + tip: &'a str, + position: tip::Position, +) -> Element<'a, Message> { + tooltip( + content, + container(text(tip).size(14)) + .padding(5) + .style(container::dark), + position, + ) + .into() +} + +pub mod text_input { + use iced::widget::text_input::{Status, Style, default}; + use iced::{Theme, color}; + + pub fn success(theme: &Theme, status: Status) -> Style { + Style { + background: color!(0x00_33_00).into(), + ..default(theme, status) + } + } + pub fn warning(theme: &Theme, status: Status) -> Style { + Style { + background: color!(0x33_33_00).into(), + ..default(theme, status) + } + } + pub fn error(theme: &Theme, status: Status) -> Style { + Style { + background: color!(0x33_00_00).into(), + ..default(theme, status) + } + } +}