diff --git a/api/src/guards/request/auth/admin.rs b/api/src/guards/request/auth/admin.rs index 8f75a31..717e41a 100644 --- a/api/src/guards/request/auth/admin.rs +++ b/api/src/guards/request/auth/admin.rs @@ -51,7 +51,7 @@ impl<'r> FromRequest<'r> for AdminAuth { match session { Ok(model) => Outcome::Success(AdminAuth(model, private_key.to_string())), Err(e) => Outcome::Failure( - (Status::from_code(e.code()).unwrap_or(Status::InternalServerError), None) + (Status::from_code(e.code()).unwrap_or(Status::Unauthorized), None) ), } diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 68a6062..b57261f 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use portfolio_core::{ crypto::random_8_char_string, - services::{admin_service::AdminService, candidate_service::CandidateService}, + services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService}, }; use requests::{AdminLoginRequest, RegisterRequest}; use rocket::http::{Cookie, Status, CookieJar}; @@ -75,22 +75,14 @@ pub async fn create_candidate( let plain_text_password = random_8_char_string(); - let candidate = CandidateService::create( + ApplicationService::create_candidate_with_parent( db, form.application_id, &plain_text_password, form.personal_id_number, ) - .await; - - if candidate.is_err() { - // TODO cleanup - let e = candidate.err().unwrap(); - return Err(Custom( - Status::from_code(e.code()).unwrap_or_default(), - e.message(), - )); - } + .await + .map_err(|e| Custom(Status::InternalServerError, e.to_string()))?; Ok(plain_text_password) } diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index 61424be..85486a8 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -1,6 +1,8 @@ use std::net::SocketAddr; -use portfolio_core::services::candidate_service::{CandidateService, UserDetails}; +use portfolio_core::candidate_details::ApplicationDetails; +use portfolio_core::services::application_service::ApplicationService; +use portfolio_core::services::candidate_service::{CandidateService}; use requests::LoginRequest; use rocket::http::{Cookie, CookieJar, Status}; use rocket::response::status::Custom; @@ -59,18 +61,18 @@ pub async fn whoami(session: CandidateAuth) -> Result> { #[post("/details", data = "
")] pub async fn fill_details( conn: Connection<'_, Db>, - details: Json, + details: Json, session: CandidateAuth, ) -> Result> { let db = conn.into_inner(); let form = details.into_inner(); - let candidate: entity::candidate::Model = session.into(); + let candidate: entity::candidate::Model = session.into(); // TODO: don't return candidate from session - let candidate = CandidateService::add_user_details(db, candidate, form).await; + let candidate_parent = ApplicationService::add_all_details(db, candidate.application, form).await; - if candidate.is_err() { + if candidate_parent.is_err() { // TODO cleanup - let e = candidate.err().unwrap(); + let e = candidate_parent.err().unwrap(); return Err(Custom( Status::from_code(e.code()).unwrap_or_default(), e.message(), @@ -85,13 +87,13 @@ pub async fn get_details( conn: Connection<'_, Db>, password_form: Json, session: CandidateAuth, -) -> Result, Custom> { +) -> Result, Custom> { let db = conn.into_inner(); let candidate: entity::candidate::Model = session.into(); let password = password_form.password.clone(); // let handle = tokio::spawn(async move { - let details = CandidateService::decrypt_details(db, candidate.application, password) + let details = ApplicationService::decrypt_all_details(db, candidate.application, password) .await .map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())); diff --git a/core/src/candidate_details.rs b/core/src/candidate_details.rs new file mode 100644 index 0000000..2bf1961 --- /dev/null +++ b/core/src/candidate_details.rs @@ -0,0 +1,203 @@ +use chrono::{NaiveDate}; +use entity::{candidate, parent}; +use serde::{Serialize, Deserialize}; + +use crate::{error::ServiceError, crypto}; + +pub const NAIVE_DATE_FMT: &str = "%Y-%m-%d"; + +#[derive(Clone)] +pub struct EncryptedString(String); + +impl EncryptedString { + pub async fn new(s: &str, recipients: &Vec<&str>) -> Result { + match crypto::encrypt_password_with_recipients(&s, &recipients).await{ + Ok(encrypted) => Ok(Self(encrypted)), + Err(_) => Err(ServiceError::CryptoEncryptFailed), + } + } + + pub async fn decrypt(&self, private_key: &String) -> Result { + match crypto::decrypt_password_with_private_key(&self.0, private_key).await { + Ok(decrypted) => Ok(decrypted), + Err(_) => Err(ServiceError::CryptoDecryptFailed), + } + } + + pub fn to_string(self) -> String { + self.0 + } +} + +impl Into for EncryptedString { + fn into(self) -> String { + self.0 + } +} + +impl TryFrom> for EncryptedString { + type Error = ServiceError; + + fn try_from(s: Option) -> Result { + match s { + Some(s) => Ok(Self(s)), + None => Err(ServiceError::CandidateDetailsNotSet), + } + } +} + +impl TryFrom> for EncryptedString { // TODO: take a look at this + type Error = ServiceError; + + fn try_from(d: Option) -> Result { + match d { + Some(d) => Ok(Self(d.to_string())), + None => Err(ServiceError::CandidateDetailsNotSet), + } + } +} + +#[derive(Clone)] +pub struct EncryptedApplicationDetails { + // Candidate + pub name: EncryptedString, + pub surname: EncryptedString, + pub birthplace: EncryptedString, + pub birthdate: EncryptedString, + pub address: EncryptedString, + pub telephone: EncryptedString, + pub citizenship: EncryptedString, + pub email: EncryptedString, + pub sex: EncryptedString, + pub study: EncryptedString, + + // Parent + pub parent_name: EncryptedString, + pub parent_surname: EncryptedString, + pub parent_telephone: EncryptedString, + pub parent_email: EncryptedString, +} + +impl EncryptedApplicationDetails { + pub async fn new(form: ApplicationDetails, recipients: Vec<&str>) -> Result { + let birthdate_str = form.birthdate.format(NAIVE_DATE_FMT).to_string(); + let d = tokio::try_join!( + EncryptedString::new(&form.name, &recipients), + EncryptedString::new(&form.surname, &recipients), + EncryptedString::new(&form.birthplace, &recipients), + EncryptedString::new(&birthdate_str, &recipients), + EncryptedString::new(&form.address, &recipients), + EncryptedString::new(&form.telephone, &recipients), + EncryptedString::new(&form.citizenship, &recipients), + EncryptedString::new(&form.email, &recipients), + EncryptedString::new(&form.sex, &recipients), + EncryptedString::new(&form.study, &recipients), + + EncryptedString::new(&form.parent_name, &recipients), + EncryptedString::new(&form.parent_surname, &recipients), + EncryptedString::new(&form.parent_telephone, &recipients), + EncryptedString::new(&form.parent_email, &recipients), + )?; + + Ok(EncryptedApplicationDetails { + name: d.0, + surname: d.1, + birthplace: d.2, + birthdate: d.3, + address: d.4, + telephone: d.5, + citizenship: d.6, + email: d.7, + sex: d.8, + study: d.9, + + parent_name: d.10, + parent_surname: d.11, + parent_telephone: d.12, + parent_email: d.13, + }) + } + + pub async fn decrypt(self, priv_key: String) -> Result { + let d = tokio::try_join!( + self.name.decrypt(&priv_key), // 0 + self.surname.decrypt(&priv_key), // 1 + self.birthplace.decrypt(&priv_key), // 2 + self.birthdate.decrypt(&priv_key), // 3 + self.address.decrypt(&priv_key), // 4 + self.telephone.decrypt(&priv_key), // 5 + self.citizenship.decrypt(&priv_key), // 6 + self.email.decrypt(&priv_key), // 7 + self.sex.decrypt(&priv_key), // 8 + self.study.decrypt(&priv_key), // 9 + + self.parent_name.decrypt(&priv_key), + self.parent_surname.decrypt(&priv_key), + self.parent_telephone.decrypt(&priv_key), + self.parent_email.decrypt(&priv_key), + )?; + + Ok(ApplicationDetails { + name: d.0, + surname: d.1, + birthplace: d.2, + birthdate: NaiveDate::parse_from_str(&d.3, NAIVE_DATE_FMT).unwrap(), // TODO + address: d.4, + telephone: d.5, + citizenship: d.6, + email: d.7, + sex: d.8, + study: d.9, + + parent_name: d.10, + parent_surname: d.11, + parent_telephone: d.12, + parent_email: d.13, + }) + } +} + +impl TryFrom<(candidate::Model, parent::Model)> for EncryptedApplicationDetails { + type Error = ServiceError; + + fn try_from((candidate, parent): (candidate::Model, parent::Model)) -> Result { + Ok(EncryptedApplicationDetails { + name: EncryptedString::try_from(candidate.name)?, + surname: EncryptedString::try_from(candidate.surname)?, + birthplace: EncryptedString::try_from(candidate.birthplace)?, + birthdate: EncryptedString::try_from(candidate.birthdate)?, + address: EncryptedString::try_from(candidate.address)?, + telephone: EncryptedString::try_from(candidate.telephone)?, + citizenship: EncryptedString::try_from(candidate.citizenship)?, + email: EncryptedString::try_from(candidate.email)?, + sex: EncryptedString::try_from(candidate.sex)?, + study: EncryptedString::try_from(candidate.study)?, + + parent_name: EncryptedString::try_from(parent.name)?, + parent_surname: EncryptedString::try_from(parent.surname)?, + parent_telephone: EncryptedString::try_from(parent.telephone)?, + parent_email: EncryptedString::try_from(parent.email)?, + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApplicationDetails { + // Candidate + pub name: String, + pub surname: String, + pub birthplace: String, + pub birthdate: NaiveDate, // TODO: User NaiveDate or String? + pub address: String, + pub telephone: String, + pub citizenship: String, + pub email: String, + pub sex: String, + pub study: String, + + // Parent + pub parent_name: String, + pub parent_surname: String, + pub parent_telephone: String, + pub parent_email: String, +} \ No newline at end of file diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index dd7ab01..8ffda50 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -1,4 +1,4 @@ -use crate::{Mutation, services::candidate_service::EncryptedUserDetails}; +use crate::{Mutation, candidate_details::{EncryptedApplicationDetails}}; use ::entity::candidate::{self}; use sea_orm::{*}; @@ -29,18 +29,19 @@ impl Mutation { pub async fn add_candidate_details( db: &DbConn, user: candidate::Model, - enc_details: EncryptedUserDetails, + enc_details: EncryptedApplicationDetails, ) -> Result { let mut user: candidate::ActiveModel = user.into(); - user.name = Set(Some(enc_details.name)); - user.surname = Set(Some(enc_details.surname)); - user.birthplace = Set(Some(enc_details.birthplace)); - user.address = Set(Some(enc_details.address)); - user.telephone = Set(Some(enc_details.telephone)); - user.citizenship = Set(Some(enc_details.citizenship)); - user.email = Set(Some(enc_details.email)); - user.sex = Set(Some(enc_details.sex)); - user.study = Set(Some(enc_details.study)); + user.name = Set(Some(enc_details.name.into())); + user.surname = Set(Some(enc_details.surname.into())); + user.birthplace = Set(Some(enc_details.birthplace.into())); + user.birthdate = Set(Some(enc_details.birthdate.into())); + user.address = Set(Some(enc_details.address.into())); + user.telephone = Set(Some(enc_details.telephone.into())); + user.citizenship = Set(Some(enc_details.citizenship.into())); + user.email = Set(Some(enc_details.email.into())); + user.sex = Set(Some(enc_details.sex.into())); + user.study = Set(Some(enc_details.study.into())); user.updated_at = Set(chrono::offset::Local::now().naive_local()); diff --git a/core/src/database/mutation/parent.rs b/core/src/database/mutation/parent.rs index 80c7a7e..a464e42 100644 --- a/core/src/database/mutation/parent.rs +++ b/core/src/database/mutation/parent.rs @@ -1,4 +1,4 @@ -use crate::Mutation; +use crate::{Mutation, candidate_details::EncryptedApplicationDetails}; use ::entity::parent::{self, Model}; use sea_orm::*; @@ -17,17 +17,14 @@ impl Mutation { pub async fn add_parent_details( db: &DbConn, - user: Model, - name: String, - surname: String, - telephone: String, - email: String, + parent: Model, + enc_details: EncryptedApplicationDetails, // TODO: use seperate struct?? ) -> Result { - let mut user: parent::ActiveModel = user.into(); - user.name = Set(Some(name)); - user.surname = Set(Some(surname)); - user.telephone = Set(Some(telephone)); - user.email = Set(Some(email)); + let mut user: parent::ActiveModel = parent.into(); + user.name = Set(Some(enc_details.parent_name.into())); + user.surname = Set(Some(enc_details.parent_surname.into())); + user.telephone = Set(Some(enc_details.parent_telephone.into())); + user.email = Set(Some(enc_details.parent_email.into())); user.updated_at = Set(chrono::offset::Local::now().naive_local()); diff --git a/core/src/database/query/mod.rs b/core/src/database/query/mod.rs index 73362b8..90575e6 100644 --- a/core/src/database/query/mod.rs +++ b/core/src/database/query/mod.rs @@ -2,4 +2,5 @@ pub struct Query; pub mod candidate; pub mod admin; -pub mod session; \ No newline at end of file +pub mod session; +pub mod parent; \ No newline at end of file diff --git a/core/src/database/query/parent.rs b/core/src/database/query/parent.rs new file mode 100644 index 0000000..7fc0429 --- /dev/null +++ b/core/src/database/query/parent.rs @@ -0,0 +1,17 @@ + +use entity::parent::Model; +use entity::parent::Entity; +use sea_orm::{DbConn, DbErr}; +use sea_orm::EntityTrait; + +use crate::Query; + +impl Query { + pub async fn find_parent_by_id( + db: &DbConn, + application_id: i32, + ) -> Result, DbErr> { + + Entity::find_by_id(application_id).one(db).await + } +} \ No newline at end of file diff --git a/core/src/error.rs b/core/src/error.rs index 428b0f9..0627fdc 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -5,7 +5,8 @@ pub enum ServiceError { ExpiredSession, JwtError, UserAlreadyExists, - UserNotFound, + CandidateNotFound, + ParentNotFound, DbError, UserNotFoundByJwtId, UserNotFoundBySessionId, @@ -24,7 +25,8 @@ impl ServiceError { ServiceError::ExpiredSession => (401, "Session expired, please login again".to_string()), ServiceError::JwtError => (500, "Error while encoding JWT".to_string()), ServiceError::UserAlreadyExists => (409, "User already exists".to_string()), - ServiceError::UserNotFound => (404, "User not found".to_string()), + ServiceError::CandidateNotFound => (404, "User not found".to_string()), + ServiceError::ParentNotFound => (500, "Parent not found".to_string()), ServiceError::DbError => (500, "Database error".to_string()), ServiceError::UserNotFoundByJwtId => (500, "User not found, please contact technical support".to_string()), ServiceError::UserNotFoundBySessionId => (500, "User not found, please contact technical support".to_string()), diff --git a/core/src/lib.rs b/core/src/lib.rs index 1121e29..c910059 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,6 +3,7 @@ pub mod crypto; pub mod filetype; pub mod services; pub mod error; +pub mod candidate_details; pub use database::mutation::*; pub use database::query::*; diff --git a/core/src/services/admin_service.rs b/core/src/services/admin_service.rs index f6c27a8..12dd925 100644 --- a/core/src/services/admin_service.rs +++ b/core/src/services/admin_service.rs @@ -20,7 +20,7 @@ impl AdminService { }; let Some(admin) = admin else { - return Err(ServiceError::UserNotFound); + return Err(ServiceError::CandidateNotFound); }; let private_key_encrypted = admin.private_key; @@ -55,7 +55,7 @@ impl AdminService { match SessionService::auth_user_session(db, session_uuid).await { Ok(user) => match user { AdminUser::Admin(admin) => Ok(admin), - AdminUser::User(_) => Err(ServiceError::DbError), + AdminUser::Candidate(_) => Err(ServiceError::DbError), }, Err(e) => Err(e), } diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs new file mode 100644 index 0000000..5793162 --- /dev/null +++ b/core/src/services/application_service.rs @@ -0,0 +1,95 @@ +use entity::{candidate, parent}; +use sea_orm::DbConn; + +use crate::{error::ServiceError, candidate_details::{ApplicationDetails, EncryptedApplicationDetails}, Query, crypto}; + +use super::{parent_service::ParentService, candidate_service::CandidateService}; + +pub struct ApplicationService; + +impl ApplicationService { + pub async fn create_candidate_with_parent( // uchazeč s maminkou 👩‍🍼 + db: &DbConn, + application_id: i32, + plain_text_password: &String, + personal_id_number: String, + ) -> Result<(candidate::Model, parent::Model), ServiceError> { + Ok( + /* tokio::try_join!( // TODO: try_join! is not working + CandidateService::create(db, application_id, plain_text_password, personal_id_number), + ParentService::create(db, application_id) + )? */ + + + ( + CandidateService::create(db, application_id, plain_text_password, personal_id_number).await?, + ParentService::create(db, application_id).await? + ) + ) + } + + pub async fn add_all_details( + db: &DbConn, + application: i32, + form: ApplicationDetails, + ) -> Result<(candidate::Model, parent::Model), ServiceError> { + let candidate = Query::find_candidate_by_id(db, application) + .await + .map_err(|_| ServiceError::DbError)? + .ok_or(ServiceError::CandidateNotFound)?; + + let parent = Query::find_parent_by_id(db, application) + .await + .map_err(|_| ServiceError::DbError)? + .ok_or(ServiceError::ParentNotFound)?; + + let Ok(admin_public_keys) = Query::get_all_admin_public_keys(db).await else { + return Err(ServiceError::DbError); + }; + + let mut admin_public_keys_refrence: Vec<&str> = + admin_public_keys.iter().map(|s| &**s).collect(); + + let mut recipients = vec![&*candidate.public_key]; + recipients.append(&mut admin_public_keys_refrence); + + let enc_details = EncryptedApplicationDetails::new(form, recipients).await?; + + Ok( + tokio::try_join!( + CandidateService::add_candidate_details(db, candidate, enc_details.clone()), + ParentService::add_parent_details(db, parent, enc_details.clone()) + )? + ) + } + + pub async fn decrypt_all_details( + db: &DbConn, + application_id: i32, + password: String, + ) -> Result { + let candidate = match Query::find_candidate_by_id(db, application_id).await { + Ok(candidate) => candidate.unwrap(), + Err(_) => return Err(ServiceError::DbError), // TODO: logging + }; + let parent = Query::find_parent_by_id(db, application_id).await.unwrap().unwrap(); + + match crypto::verify_password((&password).to_string(), candidate.code.clone()).await { + Ok(valid) => { + if !valid { + return Err(ServiceError::InvalidCredentials); + } + } + Err(_) => return Err(ServiceError::InvalidCredentials), + } + + let dec_priv_key = crypto::decrypt_password(candidate.private_key.clone(), password) + .await + .ok() + .unwrap(); + let enc_details = EncryptedApplicationDetails::try_from((candidate, parent))?; + + enc_details.decrypt(dec_priv_key).await + } + +} \ No newline at end of file diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index c63f37d..f0e1e9d 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,188 +1,16 @@ -use entity::candidate; +use entity::{candidate}; use sea_orm::{prelude::Uuid, DbConn}; -use serde::{Deserialize, Serialize}; use crate::{ crypto::{self, hash_password}, error::ServiceError, - Mutation, Query, + Mutation, Query, candidate_details::{EncryptedApplicationDetails}, }; -use super::session_service::{AdminUser, SessionService}; +use super::{session_service::{AdminUser, SessionService}}; const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; -pub(crate) struct EncryptedUserDetails { - pub name: String, - pub surname: String, - pub birthplace: String, - // pub birthdate: NaiveDate, - pub address: String, - pub telephone: String, - pub citizenship: String, - pub email: String, - pub sex: String, - pub study: String, -} - -impl EncryptedUserDetails { - pub async fn encrypt_form(form: UserDetails, recipients: Vec<&str>) -> EncryptedUserDetails { - let ( - Ok(name), - Ok(surname), - Ok(birthplace), - // Ok(enc_birthdate), - Ok(address), - Ok(telephone), - Ok(citizenship), - Ok(email), - Ok(sex), - Ok(study), - ) = tokio::join!( - crypto::encrypt_password_with_recipients(&form.name, &recipients), - crypto::encrypt_password_with_recipients(&form.surname, &recipients), - crypto::encrypt_password_with_recipients(&form.birthplace, &recipients), - // crypto::encrypt_password_with_recipients(&self.birthdate, &recipients), // TODO - crypto::encrypt_password_with_recipients(&form.address, &recipients), - crypto::encrypt_password_with_recipients(&form.telephone, &recipients), - crypto::encrypt_password_with_recipients(&form.citizenship, &recipients), - crypto::encrypt_password_with_recipients(&form.email, &recipients), - crypto::encrypt_password_with_recipients(&form.sex, &recipients), - crypto::encrypt_password_with_recipients(&form.study, &recipients), - ) else { - panic!("Failed to encrypt user details"); // TODO - }; - - EncryptedUserDetails { - name, - surname, - birthplace, - // birthdate: NaiveDate::from_ymd(2000, 1, 1), - address, - telephone, - citizenship, - email, - sex, - study, - } - } - - fn extract_enc_candidate_details( - candidate: candidate::Model, - ) -> Result { - let ( // TODO: simplify?? - Some(name), - Some(surname), - Some(birthplace), - // Some(birthdate), - Some(address), - Some(telephone), - Some(citizenship), - Some(email), - Some(sex), - Some(study), - ) = ( - candidate.name, - candidate.surname, - candidate.birthplace, - // candidate.birthdate, - candidate.address, - candidate.telephone, - candidate.citizenship, - candidate.email, - candidate.sex, - candidate.study, - ) else { - return Err(ServiceError::CandidateDetailsNotSet); - }; - - Ok(UserDetails { - name, - surname, - birthplace, - // birthdate, - address, - telephone, - citizenship, - email, - sex, - study, - }) - } - - pub fn from_model(candidate: candidate::Model) -> Result { - let Ok(details) = Self::extract_enc_candidate_details(candidate) else { - return Err(ServiceError::CandidateDetailsNotSet); - }; - Ok(EncryptedUserDetails { - name: details.name, - surname: details.surname, - birthplace: details.birthplace, - // birthdate, - address: details.address, - telephone: details.telephone, - citizenship: details.citizenship, - email: details.email, - sex: details.sex, - study: details.study, - }) - } - - pub async fn decrypt(self, priv_key: String) -> Result { - let ( - Ok(name), - Ok(surname), - Ok(birthplace), - // Ok(enc_birthdate), - Ok(address), - Ok(telephone), - Ok(citizenship), - Ok(email), - Ok(sex), - Ok(study), - ) = tokio::join!( - crypto::decrypt_password_with_private_key(&self.name, &priv_key), - crypto::decrypt_password_with_private_key(&self.surname, &priv_key), - crypto::decrypt_password_with_private_key(&self.birthplace, &priv_key), - crypto::decrypt_password_with_private_key(&self.address, &priv_key), - crypto::decrypt_password_with_private_key(&self.telephone, &priv_key), - crypto::decrypt_password_with_private_key(&self.citizenship, &priv_key), - crypto::decrypt_password_with_private_key(&self.email, &priv_key), - crypto::decrypt_password_with_private_key(&self.sex, &priv_key), - crypto::decrypt_password_with_private_key(&self.study, &priv_key), - ) else { - panic!("Failed to encrypt user details"); // TODO - }; - - Ok(UserDetails { - name, - surname, - birthplace, - // birthdate: NaiveDate::from_ymd(2000, 1, 1), - address, - telephone, - citizenship, - email, - sex, - study, - }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct UserDetails { - pub name: String, - pub surname: String, - pub birthplace: String, - // pub birthdate: NaiveDate, - pub address: String, - pub telephone: String, - pub citizenship: String, - pub email: String, - pub sex: String, - pub study: String, -} - pub struct CandidateService; impl CandidateService { @@ -191,7 +19,7 @@ impl CandidateService { /// Hashed password /// Encrypted private key /// Public key - pub async fn create( + pub(in crate::services) async fn create( db: &DbConn, application_id: i32, plain_text_password: &String, @@ -224,9 +52,6 @@ impl CandidateService { let Ok(hashed_personal_id_number) = hash_password(personal_id_number).await else { return Err(ServiceError::CryptoHashFailed); }; - /* let encrypted_personal_id_number = crypto::encrypt_password_with_recipients( - &personal_id_number, &vec![&pubkey] - ).await.unwrap(); */ Mutation::create_candidate( db, @@ -236,66 +61,25 @@ impl CandidateService { pubkey, encrypted_priv_key, ) - .await - .map_err(|_| ServiceError::DbError) - } - - pub async fn add_user_details( - db: &DbConn, - user: candidate::Model, - form: UserDetails, - ) -> Result { - let Ok(admin_public_keys) = Query::get_all_admin_public_keys(db).await else { - return Err(ServiceError::DbError); - }; - - let mut admin_public_keys_refrence: Vec<&str> = - admin_public_keys.iter().map(|s| &**s).collect(); - - let mut recipients = vec![&*user.public_key]; - - recipients.append(&mut admin_public_keys_refrence); - - let enc_details = EncryptedUserDetails::encrypt_form(form, recipients).await; - - Mutation::add_candidate_details(db, user, enc_details) .await .map_err(|_| ServiceError::DbError) } - pub async fn decrypt_details( + pub(in crate::services) async fn add_candidate_details( db: &DbConn, - candidate_id: i32, - password: String, - ) -> Result { - let candidate = match Query::find_candidate_by_id(db, candidate_id).await { - Ok(candidate) => candidate.unwrap(), - Err(_) => return Err(ServiceError::DbError), // TODO: logging - }; - - match crypto::verify_password((&password).to_string(), candidate.code.clone()).await { - Ok(valid) => { - if !valid { - return Err(ServiceError::InvalidCredentials); - } - } - Err(_) => return Err(ServiceError::InvalidCredentials), - } - - let dec_priv_key = crypto::decrypt_password(candidate.private_key.clone(), password) + candidate: candidate::Model, + enc_details: EncryptedApplicationDetails, + ) -> Result { + Mutation::add_candidate_details(db, candidate, enc_details.clone()) .await - .ok() - .unwrap(); - let enc_details = EncryptedUserDetails::from_model(candidate)?; - - enc_details.decrypt(dec_priv_key).await + .map_err(|_| ServiceError::DbError) } - pub async fn is_set_up(candidate: &candidate::Model) -> bool { + pub fn is_set_up(candidate: &candidate::Model) -> bool { candidate.name.is_some() && candidate.surname.is_some() && candidate.birthplace.is_some() && - // birthdate: NaiveDate::from_ymd(2000, 1, 1), + candidate.birthdate.is_some() && candidate.address.is_some() && candidate.telephone.is_some() && candidate.citizenship.is_some() && @@ -323,20 +107,9 @@ impl CandidateService { } async fn decrypt_private_key( - db: &DbConn, - candidate_id: i32, + candidate: candidate::Model, password: String, ) -> Result { - let candidate = Query::find_candidate_by_id(db, candidate_id).await; - - let Ok(candidate) = candidate else { - return Err(ServiceError::DbError); - }; - - let Some(candidate) = candidate else { - return Err(ServiceError::UserNotFound); - }; - let private_key_encrypted = candidate.private_key; let private_key = crypto::decrypt_password(private_key_encrypted, password).await; @@ -350,15 +123,20 @@ impl CandidateService { pub async fn login( db: &DbConn, - user_id: i32, + candidate_id: i32, password: String, ip_addr: String, ) -> Result<(String, String), ServiceError> { + let candidate = Query::find_candidate_by_id(db, candidate_id) + .await + .map_err(|_| ServiceError::DbError)? + .ok_or(ServiceError::CandidateNotFound)?; + let session_id = - SessionService::new_session(db, Some(user_id), None, password.clone(), ip_addr).await; + SessionService::new_session(db, Some(candidate_id), None, password.clone(), ip_addr).await; match session_id { Ok(session_id) => { - let private_key = Self::decrypt_private_key(db, user_id, password).await?; + let private_key = Self::decrypt_private_key(candidate, password).await?; Ok((session_id, private_key)) } Err(e) => Err(e), @@ -368,7 +146,7 @@ impl CandidateService { pub async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { match SessionService::auth_user_session(db, session_uuid).await { Ok(user) => match user { - AdminUser::User(candidate) => Ok(candidate), + AdminUser::Candidate(candidate) => Ok(candidate), AdminUser::Admin(_) => Err(ServiceError::DbError), }, Err(e) => Err(e), @@ -388,15 +166,19 @@ impl CandidateService { #[cfg(test)] mod tests { - use entity::candidate::Model; use sea_orm::{Database, DbConn}; use crate::{ crypto, - services::candidate_service::{CandidateService, UserDetails}, + services::candidate_service::{CandidateService}, Mutation, }; - use super::EncryptedUserDetails; + use super::EncryptedApplicationDetails; + use chrono::NaiveDate; + use entity::{parent, candidate}; + + use crate::services::application_service::ApplicationService; + use crate::candidate_details::ApplicationDetails; #[tokio::test] async fn test_application_id_validation() { @@ -411,7 +193,7 @@ mod tests { #[cfg(test)] async fn get_memory_sqlite_connection() -> DbConn { - use entity::{admin, candidate}; + use entity::{admin, candidate, parent}; use sea_orm::Schema; use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, DbBackend}; @@ -420,8 +202,8 @@ mod tests { let schema = Schema::new(DbBackend::Sqlite); let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); + let stmt3: TableCreateStatement = schema.create_table_from_entity(parent::Entity); db.execute(db.get_database_backend().build(&stmt)) .await @@ -429,6 +211,9 @@ mod tests { db.execute(db.get_database_backend().build(&stmt2)) .await .unwrap(); + db.execute(db.get_database_backend().build(&stmt3)) + .await + .unwrap(); db } @@ -440,11 +225,15 @@ mod tests { let secret_message = "trnka".to_string(); + let candidate = CandidateService::create(&db, 103151, &plain_text_password, "".to_string()) .await .ok() .unwrap(); + Mutation::create_parent(&db, 103151) + .await.unwrap(); + let encrypted_message = crypto::encrypt_password_with_recipients(&secret_message, &vec![&candidate.public_key]) .await @@ -464,53 +253,57 @@ mod tests { } #[cfg(test)] - async fn put_user_data(db: &DbConn) -> Model { + async fn put_user_data(db: &DbConn) -> (candidate::Model, parent::Model) { let plain_text_password = "test".to_string(); - let candidate = CandidateService::create(&db, 103151, &plain_text_password, "".to_string()) + let (candidate, parent) = ApplicationService::create_candidate_with_parent(&db, 103151, &plain_text_password, "".to_string()) .await .ok() .unwrap(); - let form = UserDetails { + let form = ApplicationDetails { name: "test".to_string(), - surname: "a".to_string(), + surname: "aaa".to_string(), birthplace: "b".to_string(), - // birthdate: NaiveDate::from_ymd(1999, 1, 1), + birthdate: NaiveDate::from_ymd(1999, 1, 1), address: "test".to_string(), telephone: "test".to_string(), citizenship: "test".to_string(), email: "test".to_string(), sex: "test".to_string(), study: "test".to_string(), + parent_name: "test".to_string(), + parent_surname: "test".to_string(), + parent_telephone: "test".to_string(), + parent_email: "test".to_string(), + }; - CandidateService::add_user_details(&db, candidate, form) + + ApplicationService::add_all_details(&db, candidate.application, form) .await - .ok() .unwrap() } #[tokio::test] async fn test_put_user_data() { let db = get_memory_sqlite_connection().await; - let candidate = put_user_data(&db).await; + let (candidate, parent) = put_user_data(&db).await; assert!(candidate.name.is_some()); + assert!(parent.name.is_some()); } #[tokio::test] async fn test_encrypt_decrypt_user_data() { let password = "test".to_string(); let db = get_memory_sqlite_connection().await; - let enc_candidate = put_user_data(&db).await; + let (enc_candidate, enc_parent) = put_user_data(&db).await; let dec_priv_key = crypto::decrypt_password(enc_candidate.private_key.clone(), password) .await .unwrap(); - let dec_candidate = EncryptedUserDetails::from_model(enc_candidate) - .unwrap() - .decrypt(dec_priv_key) - .await - .unwrap(); + let enc_details = EncryptedApplicationDetails::try_from((enc_candidate, enc_parent)).ok().unwrap(); + let dec_details = enc_details.decrypt(dec_priv_key).await.ok().unwrap(); - assert_eq!(dec_candidate.name, "test"); + assert_eq!(dec_details.name, "test"); // TODO: test every element + assert_eq!(dec_details.parent_surname, "test"); } } diff --git a/core/src/services/mod.rs b/core/src/services/mod.rs index d6cd137..28fe987 100644 --- a/core/src/services/mod.rs +++ b/core/src/services/mod.rs @@ -1,3 +1,5 @@ pub mod session_service; pub mod candidate_service; -pub mod admin_service; \ No newline at end of file +pub mod admin_service; +pub mod parent_service; +pub mod application_service; \ No newline at end of file diff --git a/core/src/services/parent_service.rs b/core/src/services/parent_service.rs new file mode 100644 index 0000000..79ce638 --- /dev/null +++ b/core/src/services/parent_service.rs @@ -0,0 +1,31 @@ +use entity::{parent}; +use sea_orm::DbConn; + +use crate::{error::ServiceError, Mutation, candidate_details::EncryptedApplicationDetails}; + +pub struct ParentService; + +impl ParentService { + pub async fn create( + db: &DbConn, + application_id: i32, + ) -> Result { + let parent = Mutation::create_parent(db, application_id) + .await + .map_err(|_| ServiceError::DbError)?; + + Ok(parent) + } + + pub async fn add_parent_details( + db: &DbConn, + parent: parent::Model, + enc_details: EncryptedApplicationDetails, + ) -> Result { + let parent = Mutation::add_parent_details(db, parent, enc_details) + .await + .map_err(|_| ServiceError::DbError)?; + + Ok(parent) + } +} \ No newline at end of file diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index f827679..a661f10 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -11,7 +11,7 @@ use crate::{ pub enum AdminUser { Admin(entity::admin::Model), - User(entity::candidate::Model), + Candidate(entity::candidate::Model), } pub(in crate::services) struct SessionService; @@ -57,7 +57,7 @@ impl SessionService { let candidate = match Query::find_candidate_by_id(db, user_id.unwrap()).await { Ok(candidate) => match candidate { Some(candidate) => candidate, - None => return Err(ServiceError::UserNotFound), + None => return Err(ServiceError::CandidateNotFound), }, Err(_) => return Err(ServiceError::DbError), }; @@ -78,7 +78,7 @@ impl SessionService { let admin = match Query::find_admin_by_id(db, admin_id.unwrap()).await { Ok(admin) => match admin { Some(admin) => admin, - None => return Err(ServiceError::UserNotFound), + None => return Err(ServiceError::CandidateNotFound), }, Err(_) => return Err(ServiceError::DbError), }; @@ -147,7 +147,7 @@ impl SessionService { if candidate.is_ok() { if let Some(candidate) = candidate.unwrap() { - return Ok(AdminUser::User(candidate)); + return Ok(AdminUser::Candidate(candidate)); } } @@ -162,7 +162,7 @@ impl SessionService { #[cfg(test)] mod tests { - use entity::{admin, candidate, session}; + use entity::{admin, candidate, session, parent}; use sea_orm::{ prelude::Uuid, sea_query::TableCreateStatement, ConnectionTrait, Database, DbBackend, @@ -171,7 +171,7 @@ mod tests { use crate::{ crypto, - services::{candidate_service::CandidateService, session_service::SessionService}, + services::{session_service::SessionService, application_service::ApplicationService}, }; #[cfg(test)] @@ -183,6 +183,7 @@ mod tests { let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity); + let stmt4: TableCreateStatement = schema.create_table_from_entity(parent::Entity); db.execute(db.get_database_backend().build(&stmt)) .await .unwrap(); @@ -192,6 +193,9 @@ mod tests { db.execute(db.get_database_backend().build(&stmt3)) .await .unwrap(); + db.execute(db.get_database_backend().build(&stmt4)) + .await + .unwrap(); db } @@ -201,10 +205,10 @@ mod tests { let db = get_memory_sqlite_connection().await; - let candidate = CandidateService::create(&db, 103151, &SECRET.to_string(), "".to_string()) + let candidate = ApplicationService::create_candidate_with_parent(&db, 103151, &SECRET.to_string(), "".to_string()) .await .ok() - .unwrap(); + .unwrap().0; assert_eq!(candidate.application, 103151); assert_ne!(candidate.code, SECRET.to_string()); @@ -218,10 +222,9 @@ mod tests { async fn test_candidate_session_correct_password() { let db = &get_memory_sqlite_connection().await; - CandidateService::create(&db, 103151, &"Tajny_kod".to_string(), "".to_string()) + ApplicationService::create_candidate_with_parent(db, 103151, &"Tajny_kod".to_string(), "".to_string()) .await - .ok() - .unwrap(); + .unwrap().0; // correct password let session = SessionService::new_session( @@ -231,9 +234,8 @@ mod tests { "Tajny_kod".to_string(), "127.0.0.1".to_string(), ) - .await - .ok() - .unwrap(); + .await + .unwrap(); // println!("{}", session.err().unwrap().1); assert!( SessionService::auth_user_session(db, Uuid::parse_str(&session).unwrap()) @@ -247,10 +249,9 @@ mod tests { let db = &get_memory_sqlite_connection().await; let candidate_form = - CandidateService::create(&db, 103151, &"Tajny_kod".to_string(), "".to_string()) + ApplicationService::create_candidate_with_parent(&db, 103151, &"Tajny_kod".to_string(), "".to_string()) .await - .ok() - .unwrap(); + .unwrap().0; // incorrect password assert!(SessionService::new_session( diff --git a/entity/src/admin.rs b/entity/src/admin.rs index cc51dae..8486730 100644 --- a/entity/src/admin.rs +++ b/entity/src/admin.rs @@ -1,9 +1,11 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "admin")] pub struct Model { - #[sea_orm(column_type = "Integer", primary_key)] + #[sea_orm(primary_key)] pub id: i32, pub name: String, pub public_key: String, diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index bf738f9..6b7d5bd 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -1,24 +1,26 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "candidate")] pub struct Model { - #[sea_orm(column_type = "Integer", primary_key, auto_increment = false)] + #[sea_orm(primary_key, auto_increment = false)] pub application: i32, pub code: String, pub name: Option, pub surname: Option, pub birth_surname: Option, pub birthplace: Option, - pub birthdate: Option, + pub birthdate: Option, pub address: Option, pub telephone: Option, pub citizenship: Option, pub email: Option, pub sex: Option, pub study: Option, - #[sea_orm(column_type = "Text", nullable)] pub personal_identification_number: Option, + #[sea_orm(column_type = "Text")] pub personal_identification_number_hash: String, pub public_key: String, pub private_key: String, @@ -28,7 +30,7 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - #[sea_orm(has_one = "super::parent::Entity")] + #[sea_orm(has_many = "super::parent::Entity")] Parent, #[sea_orm(has_many = "super::session::Entity")] Session, diff --git a/entity/src/mod.rs b/entity/src/mod.rs index ef5f522..63cf621 100644 --- a/entity/src/mod.rs +++ b/entity/src/mod.rs @@ -1,3 +1,5 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + pub mod prelude; pub mod admin; diff --git a/entity/src/parent.rs b/entity/src/parent.rs index 4d107af..f748482 100644 --- a/entity/src/parent.rs +++ b/entity/src/parent.rs @@ -1,3 +1,5 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] @@ -18,7 +20,9 @@ pub enum Relation { #[sea_orm( belongs_to = "super::candidate::Entity", from = "Column::Application", - to = "super::candidate::Column::Application" + to = "super::candidate::Column::Application", + on_update = "Cascade", + on_delete = "Cascade" )] Candidate, } diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs index af8586b..73b608f 100644 --- a/entity/src/prelude.rs +++ b/entity/src/prelude.rs @@ -1,3 +1,5 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + pub use super::admin::Entity as Admin; pub use super::candidate::Entity as Candidate; pub use super::parent::Entity as Parent; diff --git a/entity/src/session.rs b/entity/src/session.rs index f4ecf37..e0b5829 100644 --- a/entity/src/session.rs +++ b/entity/src/session.rs @@ -1,3 +1,5 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] @@ -5,10 +7,8 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, - #[sea_orm(column_type = "Integer", nullable)] - pub admin_id: Option, - #[sea_orm(column_type = "Integer", nullable)] pub user_id: Option, + pub admin_id: Option, pub ip_address: String, pub created_at: DateTime, pub expires_at: DateTime, @@ -34,16 +34,16 @@ pub enum Relation { Candidate, } -impl Related for Entity { - fn to() -> RelationDef { - Relation::Candidate.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::Admin.def() } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Candidate.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 85e2a9c..6e2081d 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -7,7 +7,7 @@ mod m20221024_134454_insert_sample_admin; mod m20221025_154422_create_session; mod m20221027_194728_session_create_user_fk; mod m20221028_194728_session_create_admin_fk; -mod m20221030_133428_parent_create_candidate_fk; +mod m20221112_112212_create_parent_candidate_fk; pub struct Migrator; #[async_trait::async_trait] @@ -21,7 +21,7 @@ impl MigratorTrait for Migrator { Box::new(m20221025_154422_create_session::Migration), Box::new(m20221027_194728_session_create_user_fk::Migration), Box::new(m20221028_194728_session_create_admin_fk::Migration), - Box::new(m20221030_133428_parent_create_candidate_fk::Migration), + Box::new(m20221112_112212_create_parent_candidate_fk::Migration), ] } } diff --git a/migration/src/m20221024_121621_create_candidate.rs b/migration/src/m20221024_121621_create_candidate.rs index 2b7bc26..66d6466 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -23,7 +23,7 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Candidate::Surname).string()) .col(ColumnDef::new(Candidate::BirthSurname).string()) .col(ColumnDef::new(Candidate::Birthplace).string()) - .col(ColumnDef::new(Candidate::Birthdate).date()) + .col(ColumnDef::new(Candidate::Birthdate).string()) .col(ColumnDef::new(Candidate::Address).string()) .col(ColumnDef::new(Candidate::Telephone).string()) .col(ColumnDef::new(Candidate::Citizenship).string()) diff --git a/migration/src/m20221030_133428_parent_create_candidate_fk.rs b/migration/src/m20221112_112212_create_parent_candidate_fk.rs similarity index 100% rename from migration/src/m20221030_133428_parent_create_candidate_fk.rs rename to migration/src/m20221112_112212_create_parent_candidate_fk.rs