From 0d8cccb12c5b736923339e8ae6f612ffaeff43dc Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 13:59:50 +0100 Subject: [PATCH 01/46] chore: output.log in .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3bddb3f..25e342a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ target/ .env -Rocket.toml \ No newline at end of file +Rocket.toml +output.log \ No newline at end of file From cf25920a5c64645d131a7f27fce0abfdb3645355 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 14:24:13 +0100 Subject: [PATCH 02/46] feat!: separate application and candidate tables --- api/src/guards/request/auth/candidate.rs | 22 +- api/src/routes/admin.rs | 19 +- api/src/routes/candidate.rs | 94 ++--- api/src/test.rs | 11 +- core/src/database/mutation/application.rs | 68 ++++ core/src/database/mutation/candidate.rs | 68 +--- core/src/database/mutation/mod.rs | 1 + core/src/database/mutation/parent.rs | 24 +- core/src/database/query/application.rs | 25 ++ core/src/database/query/candidate.rs | 3 - core/src/database/query/mod.rs | 1 + core/src/database/query/parent.rs | 3 - core/src/database/query/session.rs | 29 +- core/src/models/candidate_details.rs | 2 +- core/src/services/application_service.rs | 333 +++++++++++++++++- core/src/services/candidate_service.rs | 307 +--------------- core/src/services/parent_service.rs | 19 +- core/src/services/portfolio_service.rs | 88 +++-- core/src/services/session_service.rs | 53 +-- core/src/utils/db.rs | 8 +- entity/src/application.rs | 45 +++ entity/src/candidate.rs | 13 +- entity/src/lib.rs | 3 +- entity/src/mod.rs | 1 + entity/src/prelude.rs | 1 + entity/src/session.rs | 10 +- migration/src/lib.rs | 4 + .../src/m20221024_121621_create_candidate.rs | 8 +- ...m20221027_194728_session_create_user_fk.rs | 4 +- .../m20230114_114628_create_application.rs | 51 +++ ..._114826_create_application_candidate_fk.rs | 26 ++ 31 files changed, 766 insertions(+), 578 deletions(-) create mode 100644 core/src/database/mutation/application.rs create mode 100644 core/src/database/query/application.rs create mode 100644 entity/src/application.rs create mode 100644 migration/src/m20230114_114628_create_application.rs create mode 100644 migration/src/m20230114_114826_create_application_candidate_fk.rs diff --git a/api/src/guards/request/auth/candidate.rs b/api/src/guards/request/auth/candidate.rs index 0cd3d48..0422626 100644 --- a/api/src/guards/request/auth/candidate.rs +++ b/api/src/guards/request/auth/candidate.rs @@ -1,7 +1,7 @@ -use entity::candidate::Model as Candidate; +use entity::application::Model as Application; use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::sea_orm::prelude::Uuid; -use portfolio_core::services::candidate_service::CandidateService; +use portfolio_core::services::application_service::ApplicationService; use rocket::http::Status; use rocket::outcome::Outcome; use rocket::request::{FromRequest, Request}; @@ -9,26 +9,26 @@ use rocket::request::{FromRequest, Request}; use crate::logging::format_request; use crate::pool::Db; -pub struct CandidateAuth(Candidate, String); +pub struct ApplicationAuth(Application, String); -impl Into for CandidateAuth { - fn into(self) -> Candidate { +impl Into for ApplicationAuth { + fn into(self) -> Application { self.0 } } -impl CandidateAuth { +impl ApplicationAuth { pub fn get_private_key(&self) -> String { self.1.clone() } } #[rocket::async_trait] -impl<'r> FromRequest<'r> for CandidateAuth { +impl<'r> FromRequest<'r> for ApplicationAuth { type Error = Option; async fn from_request( req: &'r Request<'_>, - ) -> Outcome { + ) -> Outcome { let cookie_id = req.cookies().get_private("id"); let cookie_private_key = req.cookies().get_private("key"); @@ -50,12 +50,12 @@ impl<'r> FromRequest<'r> for CandidateAuth { Err(_) => return Outcome::Failure((Status::BadRequest, None)), }; - let session = CandidateService::auth(conn, uuid).await; + let session = ApplicationService::auth(conn, uuid).await; match session { Ok(model) => { - info!("{}: CANDIDATE {} AUTHENTICATED", format_request(req), model.application); - Outcome::Success(CandidateAuth(model, private_key.to_string().to_string())) + info!("{}: CANDIDATE {} AUTHENTICATED", format_request(req), model.id); + Outcome::Success(ApplicationAuth(model, private_key.to_string().to_string())) }, Err(e) => { info!("{}: CANDIDATE {} AUTHENTICATION FAILED", format_request(req), e); diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 7791c7a..f297399 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -93,12 +93,7 @@ pub async fn create_candidate( let plain_text_password = random_12_char_string(); - ApplicationService::create_candidate_with_parent( - db, - form.application_id, - &plain_text_password, - form.personal_id_number.clone(), - ) + let application = ApplicationService::create(&db, form.application_id, &plain_text_password, form.personal_id_number.clone()) .await .map_err(to_custom_error)?; @@ -205,17 +200,19 @@ pub async fn reset_candidate_password( conn: Connection<'_, Db>, session: AdminAuth, id: i32, -) -> Result, Custom> { - let db = conn.into_inner(); +) -> Result<(), Custom> { + // TODO + /* let db = conn.into_inner(); let private_key = session.get_private_key(); let response = CandidateService::reset_password(private_key, db, id) .await - .map_err(to_custom_error)?; + .map_err(to_custom_error)?; */ - Ok( + Ok(()) + /* Ok( Json(response) - ) + ) */ } #[get("/candidate//portfolio")] diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index c565bed..7a01ce7 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -1,5 +1,6 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use entity::application; use portfolio_core::Query; use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::models::candidate::{ApplicationDetails, NewCandidateResponse}; @@ -16,7 +17,7 @@ use sea_orm_rocket::Connection; use crate::guards::data::letter::Letter; use crate::guards::data::portfolio::Portfolio; -use crate::{guards::request::auth::CandidateAuth, pool::Db, requests}; +use crate::{guards::request::auth::ApplicationAuth, pool::Db, requests}; use super::to_custom_error; @@ -29,7 +30,7 @@ pub async fn login( ) -> Result<(), Custom> { let ip_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let db = conn.into_inner(); - let (session_token, private_key) = CandidateService::login( + let (session_token, private_key) = ApplicationService::login( db, login_form.application_id, login_form.password.to_string(), @@ -47,7 +48,7 @@ pub async fn login( #[post("/logout")] pub async fn logout( conn: Connection<'_, Db>, - _session: CandidateAuth, + _session: ApplicationAuth, cookies: &CookieJar<'_>, ) -> Result<(), Custom> { let db = conn.into_inner(); @@ -61,7 +62,7 @@ pub async fn logout( let session_id = Uuid::try_parse(cookie.value()) // unwrap would be safe here because of the auth guard .map_err(|e| Custom(Status::BadRequest, e.to_string()))?; let session = Query::find_session_by_uuid(db, session_id).await.unwrap().unwrap(); // TODO - CandidateService::logout(db, session) + ApplicationService::logout(db, session) .await .map_err(to_custom_error)?; @@ -72,9 +73,13 @@ pub async fn logout( } #[get("/whoami")] -pub async fn whoami(session: CandidateAuth) -> Result, Custom> { +pub async fn whoami(conn: Connection<'_, Db>, session: ApplicationAuth) -> Result, Custom> { + let db = conn.into_inner(); + let private_key = session.get_private_key(); - let candidate: entity::candidate::Model = session.into(); + let application: entity::application::Model = session.into(); + let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO + println!("candidate: {:?}", candidate); let response = NewCandidateResponse::from_encrypted(&private_key, candidate).await .map_err(to_custom_error)?; @@ -86,13 +91,14 @@ pub async fn whoami(session: CandidateAuth) -> Result pub async fn post_details( conn: Connection<'_, Db>, details: Json, - session: CandidateAuth, + session: ApplicationAuth, ) -> Result, Custom> { let db = conn.into_inner(); let form = details.into_inner(); - let candidate: entity::candidate::Model = session.into(); + let application: application::Model = session.into(); + let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO - let _candidate_parent = ApplicationService::add_all_details(db, candidate, &form) + let _candidate_parent = ApplicationService::add_all_details(db, &application.public_key, candidate, &form) .await .map_err(to_custom_error)?; @@ -102,11 +108,12 @@ pub async fn post_details( #[get("/details")] pub async fn get_details( conn: Connection<'_, Db>, - session: CandidateAuth, + session: ApplicationAuth, ) -> Result, Custom> { let db = conn.into_inner(); let private_key = session.get_private_key(); - let candidate: entity::candidate::Model = session.into(); + let application: entity::application::Model = session.into(); + let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO let details = ApplicationService::decrypt_all_details(private_key, db, candidate) .await @@ -117,12 +124,12 @@ pub async fn get_details( } #[post("/cover_letter", data = "")] pub async fn upload_cover_letter( - session: CandidateAuth, + session: ApplicationAuth, letter: Letter, ) -> Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); + let candidate: entity::application::Model = session.into(); - PortfolioService::add_cover_letter_to_cache(candidate.application, letter.into()) + PortfolioService::add_cover_letter_to_cache(candidate.id, letter.into()) .await .map_err(to_custom_error)?; @@ -130,10 +137,10 @@ pub async fn upload_cover_letter( } #[delete("/cover_letter")] -pub async fn delete_cover_letter(session: CandidateAuth) -> Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); +pub async fn delete_cover_letter(session: ApplicationAuth) -> Result<(), Custom> { + let candidate: entity::application::Model = session.into(); - PortfolioService::delete_cover_letter_from_cache(candidate.application) + PortfolioService::delete_cover_letter_from_cache(candidate.id) .await .map_err(to_custom_error)?; @@ -142,12 +149,12 @@ pub async fn delete_cover_letter(session: CandidateAuth) -> Result<(), Custom Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); + let candidate: entity::application::Model = session.into(); - PortfolioService::add_portfolio_letter_to_cache(candidate.application, letter.into()) + PortfolioService::add_portfolio_letter_to_cache(candidate.id, letter.into()) .await .map_err(to_custom_error)?; @@ -155,10 +162,10 @@ pub async fn upload_portfolio_letter( } #[delete("/portfolio_letter")] -pub async fn delete_portfolio_letter(session: CandidateAuth) -> Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); +pub async fn delete_portfolio_letter(session: ApplicationAuth) -> Result<(), Custom> { + let candidate: entity::application::Model = session.into(); - PortfolioService::delete_portfolio_letter_from_cache(candidate.application) + PortfolioService::delete_portfolio_letter_from_cache(candidate.id) .await .map_err(to_custom_error)?; @@ -167,12 +174,12 @@ pub async fn delete_portfolio_letter(session: CandidateAuth) -> Result<(), Custo #[post("/portfolio_zip", data = "")] pub async fn upload_portfolio_zip( - session: CandidateAuth, + session: ApplicationAuth, portfolio: Portfolio, ) -> Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); + let candidate: entity::application::Model = session.into(); - PortfolioService::add_portfolio_zip_to_cache(candidate.application, portfolio.into()) + PortfolioService::add_portfolio_zip_to_cache(candidate.id, portfolio.into()) .await .map_err(to_custom_error)?; @@ -180,10 +187,10 @@ pub async fn upload_portfolio_zip( } #[delete("/portfolio_zip")] -pub async fn delete_portfolio_zip(session: CandidateAuth) -> Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); +pub async fn delete_portfolio_zip(session: ApplicationAuth) -> Result<(), Custom> { + let candidate: entity::application::Model = session.into(); - PortfolioService::delete_portfolio_zip_from_cache(candidate.application) + PortfolioService::delete_portfolio_zip_from_cache(candidate.id) .await .map_err(to_custom_error)?; @@ -192,11 +199,11 @@ pub async fn delete_portfolio_zip(session: CandidateAuth) -> Result<(), Custom Result, Custom> { - let candidate: entity::candidate::Model = session.into(); + let candidate: entity::application::Model = session.into(); - let progress = PortfolioService::get_submission_progress(candidate.application) + let progress = PortfolioService::get_submission_progress(candidate.id) .await .map(|x| Json(x)) .map_err(to_custom_error); @@ -207,20 +214,21 @@ pub async fn submission_progress( #[post("/submit")] pub async fn submit_portfolio( conn: Connection<'_, Db>, - session: CandidateAuth, + session: ApplicationAuth, ) -> Result<(), Custom> { let db = conn.into_inner(); - let candidate: entity::candidate::Model = session.into(); + let application: entity::application::Model = session.into(); + let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO - let submit = PortfolioService::submit(&candidate, &db).await; + let submit = PortfolioService::submit(&application.public_key, &candidate, &db).await; if submit.is_err() { let e = submit.err().unwrap(); // Delete on critical error if e.code() == 500 { // Cleanup - PortfolioService::delete_portfolio(candidate.application) + PortfolioService::delete_portfolio(application.id) .await .unwrap(); } @@ -232,11 +240,11 @@ pub async fn submit_portfolio( #[post("/delete")] pub async fn delete_portfolio( - session: CandidateAuth, + session: ApplicationAuth, ) -> Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); + let candidate: entity::application::Model = session.into(); - PortfolioService::delete_portfolio(candidate.application) + PortfolioService::delete_portfolio(candidate.id) .await .map_err(to_custom_error)?; @@ -244,11 +252,11 @@ pub async fn delete_portfolio( } #[get("/download")] -pub async fn download_portfolio(session: CandidateAuth) -> Result, Custom> { +pub async fn download_portfolio(session: ApplicationAuth) -> Result, Custom> { let private_key = session.get_private_key(); - let candidate: entity::candidate::Model = session.into(); + let candidate: entity::application::Model = session.into(); - let file = PortfolioService::get_portfolio(candidate.application, private_key) + let file = PortfolioService::get_portfolio(candidate.id, private_key) .await .map_err(to_custom_error); @@ -331,7 +339,7 @@ mod tests { assert_eq!(response.status(), Status::Ok); let candidate = response.into_json::().unwrap(); - assert_eq!(candidate.application_id, APPLICATION_ID); + // assert_eq!(candidate.id, APPLICATION_ID); // TODO assert_eq!(candidate.personal_id_number, PERSONAL_ID_NUMBER); } diff --git a/api/src/test.rs b/api/src/test.rs index 919c206..98d2ad9 100644 --- a/api/src/test.rs +++ b/api/src/test.rs @@ -42,14 +42,21 @@ pub mod tests { .await .unwrap(); - ApplicationService::create_candidate_with_parent( + let application = ApplicationService::create( db, APPLICATION_ID, &CANDIDATE_PASSWORD.to_string(), + PERSONAL_ID_NUMBER.to_string()) + .await.unwrap(); + + /* ApplicationService::create_candidate_with_parent( + db, + application, + &CANDIDATE_PASSWORD.to_string(), PERSONAL_ID_NUMBER.to_string(), ) .await - .unwrap(); + .unwrap(); */ } pub fn test_client() -> &'static Mutex { diff --git a/core/src/database/mutation/application.rs b/core/src/database/mutation/application.rs new file mode 100644 index 0000000..e39fcbb --- /dev/null +++ b/core/src/database/mutation/application.rs @@ -0,0 +1,68 @@ +use ::entity::application; +use log::{info, warn}; +use sea_orm::{DbConn, DbErr, Set, ActiveModelTrait, EntityTrait, IntoActiveModel}; + +use crate::Mutation; + +impl Mutation { + pub async fn create_application( + db: &DbConn, + application_id: i32, + candidate_id: i32, + hashed_password: String, + enc_personal_id_number: String, + pubkey: String, + encrypted_priv_key: String, + ) -> Result { + let insert = application::ActiveModel { + id: Set(application_id), + personal_id_number: Set(enc_personal_id_number), + password: Set(hashed_password), + candidate_id: Set(candidate_id), + public_key: Set(pubkey), + private_key: Set(encrypted_priv_key), + created_at: Set(chrono::offset::Local::now().naive_local()), + updated_at: Set(chrono::offset::Local::now().naive_local()), + ..Default::default() + } + .insert(db) + .await?; + + info!("APPLICATION {} CREATED", application_id); + Ok(insert) + } + + pub async fn update_candidate_fk( + db: &DbConn, + application: application::Model, + candidate_id: i32, + ) -> Result { + let application_id = application.id; + let mut application = application.into_active_model(); + application.candidate_id = Set(candidate_id); + + let update = application.update(db).await?; + + warn!("CANDIDATE {} FK UPDATED", application_id); + Ok(update) + } + + pub async fn update_application_password_and_keys( + db: &DbConn, + application: application::Model, + new_password_hash: String, + pub_key: String, + priv_key_enc: String, + ) -> Result { + let application_id = application.id; + let mut application = application.into_active_model(); + application.password = Set(new_password_hash); + application.public_key = Set(pub_key); + application.private_key = Set(priv_key_enc); + + let update = application.update(db).await?; + + warn!("CANDIDATE {} PASSWORD CHANGED", application_id); + Ok(update) + } +} \ No newline at end of file diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 5e1edff..77511b4 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -1,24 +1,16 @@ use crate::{Mutation, models::candidate_details::{EncryptedCandidateDetails}}; -use ::entity::candidate::{self}; +use ::entity::{candidate::{self}, application}; use log::{info, warn}; use sea_orm::*; impl Mutation { pub async fn create_candidate( db: &DbConn, - application_id: i32, - hashed_password: String, enc_personal_id_number: String, - pubkey: String, - encrypted_priv_key: String, ) -> Result { - let insert = candidate::ActiveModel { - application: Set(application_id), + let candidate = candidate::ActiveModel { personal_identification_number: Set(enc_personal_id_number), - code: Set(hashed_password), - public_key: Set(pubkey), - private_key: Set(encrypted_priv_key), created_at: Set(chrono::offset::Local::now().naive_local()), updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() @@ -26,8 +18,17 @@ impl Mutation { .insert(db) .await?; - info!("CANDIDATE {} CREATED", application_id); - Ok(insert) + info!("CANDIDATE {} CREATED", candidate.application); + Ok(candidate) + } + + pub async fn find_related_application( + db: &DbConn, + candidate: candidate::Model, + ) -> Result, DbErr> { + candidate.find_related(application::Entity) + .one(db) + .await } pub async fn delete_candidate( @@ -41,25 +42,6 @@ impl Mutation { Ok(delete) } - pub async fn update_candidate_password_and_keys( - db: &DbConn, - candidate: candidate::Model, - new_password_hash: String, - pub_key: String, - priv_key_enc: String, - ) -> Result { - let application = candidate.application; - let mut candidate: candidate::ActiveModel = candidate.into(); - candidate.code = Set(new_password_hash); - candidate.public_key = Set(pub_key); - candidate.private_key = Set(priv_key_enc); - - let update = candidate.update(db).await?; - - warn!("CANDIDATE {} PASSWORD CHANGED", application); - Ok(update) - } - pub async fn update_candidate_details( db: &DbConn, user: candidate::Model, @@ -103,20 +85,14 @@ mod tests { async fn test_create_candidate() { let db = get_memory_sqlite_connection().await; - const APPLICATION_ID: i32 = 103158; - - Mutation::create_candidate( + let candidate = Mutation::create_candidate( &db, - APPLICATION_ID, - "test".to_string(), - "test".to_string(), - "test".to_string(), - "test".to_string(), + "".to_string(), ) .await .unwrap(); - let candidate = Query::find_candidate_by_id(&db, APPLICATION_ID) + let candidate = Query::find_candidate_by_id(&db, candidate.application) .await .unwrap(); assert!(candidate.is_some()); @@ -126,15 +102,9 @@ mod tests { async fn test_add_candidate_details() { let db = get_memory_sqlite_connection().await; - const APPLICATION_ID: i32 = 103158; - let candidate = Mutation::create_candidate( &db, - APPLICATION_ID, - "test".to_string(), - "test".to_string(), - "test".to_string(), - "test".to_string(), + "".to_string(), ) .await .unwrap(); @@ -144,9 +114,9 @@ mod tests { vec!["age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_string()], ).await.unwrap(); - Mutation::update_candidate_details(&db, candidate, encrypted_details.candidate).await.unwrap(); + let candidate = Mutation::update_candidate_details(&db, candidate, encrypted_details.candidate).await.unwrap(); - let candidate = Query::find_candidate_by_id(&db, APPLICATION_ID) + let candidate = Query::find_candidate_by_id(&db, candidate.application) .await .unwrap().unwrap(); diff --git a/core/src/database/mutation/mod.rs b/core/src/database/mutation/mod.rs index f5afed2..ef22e13 100644 --- a/core/src/database/mutation/mod.rs +++ b/core/src/database/mutation/mod.rs @@ -1,5 +1,6 @@ pub(crate) struct Mutation; +pub mod application; pub mod session; pub mod candidate; pub mod parent; diff --git a/core/src/database/mutation/parent.rs b/core/src/database/mutation/parent.rs index b31b269..ccf308e 100644 --- a/core/src/database/mutation/parent.rs +++ b/core/src/database/mutation/parent.rs @@ -49,20 +49,14 @@ mod tests { async fn test_create_parent() { let db = get_memory_sqlite_connection().await; - const APPLICATION_ID: i32 = 103158; - - Mutation::create_candidate( + let candidate = Mutation::create_candidate( &db, - APPLICATION_ID, - "test".to_string(), - "test".to_string(), - "test".to_string(), - "test".to_string(), + "".to_string(), ) .await .unwrap(); - let new_parent = Mutation::create_parent(&db, APPLICATION_ID).await.unwrap(); + let new_parent = Mutation::create_parent(&db, candidate.application).await.unwrap(); let parent = Query::find_parent_by_id(&db, new_parent.id).await.unwrap(); assert!(parent.is_some()); @@ -72,20 +66,14 @@ mod tests { async fn test_add_candidate_details() { let db = get_memory_sqlite_connection().await; - const APPLICATION_ID: i32 = 103158; - - Mutation::create_candidate( + let candidate = Mutation::create_candidate( &db, - APPLICATION_ID, - "test".to_string(), - "test".to_string(), - "test".to_string(), - "test".to_string(), + "".to_string(), ) .await .unwrap(); - let parent = Mutation::create_parent(&db, APPLICATION_ID).await.unwrap(); + let parent = Mutation::create_parent(&db, candidate.application).await.unwrap(); let encrypted_details: EncryptedApplicationDetails = EncryptedApplicationDetails::new( &APPLICATION_DETAILS.lock().unwrap().clone(), diff --git a/core/src/database/query/application.rs b/core/src/database/query/application.rs new file mode 100644 index 0000000..dda7f46 --- /dev/null +++ b/core/src/database/query/application.rs @@ -0,0 +1,25 @@ +use entity::{application, candidate}; +use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait}; + +use crate::Query; + +impl Query { + pub async fn find_application_by_id( + db: &DbConn, + application_id: i32, + ) -> Result, DbErr> { + application::Entity::find_by_id(application_id) + .one(db) + .await + } + + pub async fn find_related_candidate( + db: &DbConn, + application: application::Model, + ) -> Result, DbErr> { + application + .find_related(candidate::Entity) + .one(db) + .await + } +} \ No newline at end of file diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index 38d112c..feb8ec8 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -98,9 +98,6 @@ mod tests { let db = get_memory_sqlite_connection().await; let candidate = candidate::ActiveModel { application: Set(103158), - code: Set("test".to_string()), - public_key: Set("test".to_string()), - private_key: Set("test".to_string()), personal_identification_number: Set("test".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), 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 90575e6..abb5b3b 100644 --- a/core/src/database/query/mod.rs +++ b/core/src/database/query/mod.rs @@ -1,5 +1,6 @@ pub struct Query; +pub mod application; pub mod candidate; pub mod admin; pub mod session; diff --git a/core/src/database/query/parent.rs b/core/src/database/query/parent.rs index a325458..72a8b19 100644 --- a/core/src/database/query/parent.rs +++ b/core/src/database/query/parent.rs @@ -46,9 +46,6 @@ mod tests { candidate::ActiveModel { application: Set(APPLICATION_ID), - code: Set("test".to_string()), - public_key: Set("test".to_string()), - private_key: Set("test".to_string()), personal_identification_number: Set("test".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), updated_at: Set(chrono::offset::Local::now().naive_local()), diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index a9223fc..0e86509 100644 --- a/core/src/database/query/session.rs +++ b/core/src/database/query/session.rs @@ -1,7 +1,7 @@ use crate::Query; use ::entity::prelude::AdminSession; -use ::entity::{candidate, admin, admin_session}; +use ::entity::{candidate, admin, admin_session, application}; use ::entity::{session, session::Entity as Session}; use sea_orm::prelude::Uuid; use sea_orm::*; @@ -21,8 +21,8 @@ impl Query { AdminSession::find_by_id(uuid).one(db).await } - pub async fn find_related_candidate_sessions(db: &DbConn, candidate: &candidate::Model) -> Result, DbErr> { - candidate.find_related(Session) + pub async fn find_related_application_sessions(db: &DbConn, application: &application::Model) -> Result, DbErr> { + application.find_related(Session) .order_by_asc(session::Column::UpdatedAt) .all(db) .await @@ -38,11 +38,12 @@ impl Query { #[cfg(test)] mod tests { - use entity::{session, admin, candidate, admin_session}; + use entity::{session, admin, candidate, admin_session, application}; use sea_orm::{prelude::Uuid, ActiveModelTrait, Set}; + use crate::services::candidate_service::tests::put_user_data; use crate::utils::db::get_memory_sqlite_connection; - use crate::Query; + use crate::{Query, Mutation}; #[tokio::test] async fn test_find_session_by_uuid() { @@ -70,23 +71,11 @@ mod tests { const APPLICATION_ID: i32 = 103158; - let candidate = candidate::ActiveModel { - application: Set(APPLICATION_ID), - code: Set("test".to_string()), - public_key: Set("test".to_string()), - private_key: Set("test".to_string()), - personal_identification_number: Set("test".to_string()), - created_at: Set(chrono::offset::Local::now().naive_local()), - updated_at: Set(chrono::offset::Local::now().naive_local()), - ..Default::default() - } - .insert(&db) - .await - .unwrap(); + let (application, _, _) = put_user_data(&db).await; session::ActiveModel { id: Set(Uuid::new_v4()), - candidate_id: Set(Some(APPLICATION_ID)), + candidate_id: Set(Some(application.id)), ip_address: Set("10.10.10.10".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), expires_at: Set(chrono::offset::Local::now().naive_local()), @@ -126,7 +115,7 @@ mod tests { .await .unwrap(); - let sessions = Query::find_related_candidate_sessions(&db, &candidate).await.unwrap(); + let sessions = Query::find_related_application_sessions(&db, &application).await.unwrap(); assert_eq!(sessions.len(), 1); let sessions = Query::find_related_admin_sessions(&db, &admin).await.unwrap(); diff --git a/core/src/models/candidate_details.rs b/core/src/models/candidate_details.rs index c11ab68..4a55f89 100644 --- a/core/src/models/candidate_details.rs +++ b/core/src/models/candidate_details.rs @@ -510,7 +510,7 @@ pub mod tests { let db = get_memory_sqlite_connection().await; let _admin = insert_test_admin(&db).await; - let (candidate, parents) = put_user_data(&db).await; + let (application, candidate, parents) = put_user_data(&db).await; let encrypted_details = EncryptedApplicationDetails::try_from((&candidate, parents)).unwrap(); diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 63b62ea..43cd54e 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -1,34 +1,123 @@ -use entity::{candidate, parent}; -use sea_orm::DbConn; +use async_trait::async_trait; +use chrono::Duration; +use entity::{candidate, parent, application, session}; +use sea_orm::{DbConn, prelude::Uuid, IntoActiveModel}; -use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::candidate::ApplicationDetails}; +use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::{candidate::ApplicationDetails, candidate_details::EncryptedString, auth::AuthenticableTrait}, Mutation, crypto::{hash_password, self}}; -use super::{parent_service::ParentService, candidate_service::CandidateService}; +use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService}; + +const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; pub struct ApplicationService; impl ApplicationService { - pub async fn create_candidate_with_parent( // uchazeč s maminkou 👩‍🍼 + /// Creates a new candidate with: + /// Encrypted personal identification number + /// Hashed password + /// Encrypted private key + /// Public key + pub async fn create( db: &DbConn, application_id: i32, plain_text_password: &String, personal_id_number: String, - ) -> Result<(candidate::Model, parent::Model), ServiceError> { + ) -> Result { + // Check if application id starts with 101, 102 or 103 + if !Self::is_application_id_valid(application_id) { + return Err(ServiceError::InvalidApplicationId); + } + + // Check if user with that application id already exists + if Query::find_application_by_id(db, application_id) + .await? + .is_some() + { + return Err(ServiceError::UserAlreadyExists); + } + + + let hashed_password = hash_password(plain_text_password.to_string()).await?; + let (pubkey, priv_key_plain_text) = crypto::create_identity(); + let encrypted_priv_key = crypto::encrypt_password( + priv_key_plain_text, + plain_text_password.to_string() + ).await?; + + let recipients = get_recipients(db, &pubkey).await?; + let enc_personal_id_number = EncryptedString::new( + &personal_id_number, + &recipients, + ).await?; + + let candidate = CandidateService::create( + db, + enc_personal_id_number.clone().to_string() + ).await?; + + println!("candidate: {:?}", candidate); + + let application = Mutation::create_application( + db, + application_id, + candidate.application, + hashed_password, + enc_personal_id_number.to_string(), + pubkey, + encrypted_priv_key, + ).await?; + + Ok(application) + } + + fn is_application_id_valid(application_id: i32) -> bool { + let s = &application_id.to_string(); + if s.len() <= 3 { + // TODO: does the field of study prefix have to be exactly 6 digits? VYRESIT PODLE PRIHLASEK!!! + return false; + } + let field_of_study_prefix = &s[0..3]; + FIELD_OF_STUDY_PREFIXES.contains(&field_of_study_prefix) + } + + pub async fn create_candidate_with_parent( // uchazeč s maminkou 👩‍🍼 + db: &DbConn, + application: application::Model, + plain_text_password: &String, + personal_id_number: String, + ) -> Result<(application::Model, candidate::Model, parent::Model), ServiceError> { + let candidate = CandidateService::create(db, personal_id_number).await?; + let parent = ParentService::create(db, candidate.application).await?; + let application = Mutation::update_candidate_fk(db, application, candidate.application).await?; Ok( ( - CandidateService::create(db, application_id, plain_text_password, personal_id_number).await?, - ParentService::create(db, application_id).await? + application, + candidate, + parent ) ) } + pub async fn find_related_candidate( + db: &DbConn, + application: application::Model, + ) -> Result { + let candidate = Query::find_related_candidate(db, application).await?; + if let Some(candidate) = candidate { + Ok(candidate) + } else { + Err(ServiceError::CandidateNotFound) + } + } + pub async fn add_all_details( db: &DbConn, + public_key: &String, candidate: candidate::Model, form: &ApplicationDetails, ) -> Result<(candidate::Model, Vec), ServiceError> { - let recipients = get_recipients(db, &candidate.public_key).await?; + let recipients = get_recipients(db, public_key).await?; let candidate = CandidateService::add_candidate_details(db, candidate, &form.candidate, &recipients).await?; let parents = ParentService::add_parents_details(db, &candidate, &form.parents, &recipients).await?; Ok( @@ -54,5 +143,229 @@ impl ApplicationService { } } - + + async fn decrypt_private_key( + application: application::Model, + password: String, + ) -> Result { + let private_key_encrypted = application.private_key; + + let private_key = crypto::decrypt_password(private_key_encrypted, password).await?; + + Ok(private_key) + } + + pub async fn extend_session_duration_to_14_days(db: &DbConn, session: session::Model) -> Result { + let now = chrono::Utc::now().naive_utc(); + if now >= session.updated_at.checked_add_signed(Duration::days(1)).ok_or(ServiceError::Unauthorized)? { + let new_expires_at = now.checked_add_signed(Duration::days(14)).ok_or(ServiceError::Unauthorized)?; + + Ok(Mutation::update_session_expiration(db, session, new_expires_at).await?) + } else { + Ok(session) + } + } + + // TODO + /* pub async fn reset_password( + admin_private_key: String, + db: &DbConn, + id: i32, + ) -> Result { + let candidate = Query::find_candidate_by_id(db, id).await? + .ok_or(ServiceError::CandidateNotFound)?; + let parents = Query::find_candidate_parents(db, &candidate).await?; + + + let new_password_plain = crypto::random_12_char_string(); + let new_password_hash = crypto::hash_password(new_password_plain.clone()).await?; + + let (pubkey, priv_key_plain_text) = crypto::create_identity(); + let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text.clone(), + new_password_plain.to_string() + ).await?; + + + Self::delete_old_sessions(db, &candidate, 0).await?; + let candidate = Mutation::update_candidate_password_and_keys(db, + candidate, + new_password_hash, + pubkey.clone(), + encrypted_priv_key + ).await?; + + + // user might no have filled his details yet, but personal id number is filled from beginning + let personal_id_number = EncryptedString::from(candidate.personal_identification_number.clone()) + .decrypt(&admin_private_key) + .await?; + + let recipients = get_recipients(db, &pubkey).await?; + + let dec_details = EncryptedApplicationDetails::from((&candidate, parents.clone())) + .decrypt(admin_private_key).await?; + let enc_details = EncryptedApplicationDetails::new(&dec_details, recipients).await?; + + Mutation::update_candidate_details(db, candidate, enc_details.candidate).await?; + for i in 0..enc_details.parents.len() { + Mutation::add_parent_details(db, parents[i].clone(), enc_details.parents[i].clone()).await?; + } + + Ok( + CreateCandidateResponse { + application_id: id, + personal_id_number, + password: new_password_plain, + } + ) + } */ +} + +#[async_trait] +impl AuthenticableTrait for ApplicationService { + type User = application::Model; + type Session = session::Model; + + async fn login( + db: &DbConn, + application_id: i32, + password: String, + ip_addr: String, + ) -> Result<(String, String), ServiceError> { + let application = Query::find_application_by_id(db, application_id) + .await? + .ok_or(ServiceError::CandidateNotFound)?; + + let session_id = Self::new_session(db, &application, password.clone(), ip_addr).await?; + + let private_key = Self::decrypt_private_key(application, password).await?; + Ok((session_id, private_key)) + } + + async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { + let session = Query::find_session_by_uuid(db, session_uuid) + .await? + .ok_or(ServiceError::Unauthorized)?; + + if !SessionService::is_valid(&session).await? { + Mutation::delete_session(db, session.into_active_model()).await?; + return Err(ServiceError::ExpiredSession); + } + // Candidate authenticated + + Self::extend_session_duration_to_14_days(db, session.clone()).await?; + + let application = Query::find_application_by_id(db, session.candidate_id.unwrap()) + .await? + .ok_or(ServiceError::CandidateNotFound)?; + + Ok(application) + } + + async fn logout(db: &DbConn, session: session::Model) -> Result<(), ServiceError> { + Mutation::delete_session(db, session.into_active_model()).await?; + Ok(()) + } + + async fn new_session( + db: &DbConn, + application: &application::Model, + password: String, + ip_addr: String, + ) -> Result { + if !crypto::verify_password(password.clone(), application.password.clone()).await? { + return Err(ServiceError::InvalidCredentials); + } + // user is authenticated, generate a new session + let random_uuid: Uuid = Uuid::new_v4(); + + let session = Mutation::insert_candidate_session(db, random_uuid, application.id, ip_addr).await?; + + Self::delete_old_sessions(db, &application, 3).await?; + + Ok(session.id.to_string()) + } + async fn delete_old_sessions( + db: &DbConn, + application: &application::Model, + keep_n_recent: usize, + ) -> Result<(), ServiceError> { + let sessions = Query::find_related_application_sessions(db, &application) + .await? + .iter() + .map(|s| s.to_owned().into_active_model()) + .collect(); + + SessionService::delete_sessions(db, sessions, keep_n_recent).await?; + Ok(()) + } +} + +mod tests { + use crate::{utils::db::get_memory_sqlite_connection, services::{application_service::ApplicationService}, crypto}; + + const APPLICATION_ID: i32 = 103151; + #[tokio::test] + async fn test_application_id_validation() { + assert!(ApplicationService::is_application_id_valid(101_101)); + assert!(ApplicationService::is_application_id_valid(102_107)); + assert!(ApplicationService::is_application_id_valid(103_109)); + assert!(!ApplicationService::is_application_id_valid(104_109)); + assert!(!ApplicationService::is_application_id_valid(100_109)); + assert!(!ApplicationService::is_application_id_valid(201_109)); + assert!(!ApplicationService::is_application_id_valid(101)); + } + + // TODO + /* #[tokio::test] + async fn test_password_reset() { + let db = get_memory_sqlite_connection().await; + let admin = create_admin(&db).await; + let (candidate, _parent) = put_user_data(&db).await; + + let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); + + assert!( + CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_ok() + ); + + let new_password = CandidateService::reset_password(private_key, &db, candidate.application).await.unwrap().password; + + assert!( + CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_err() + ); + + assert!( + CandidateService::login(&db, candidate.application, new_password, "127.0.0.1".to_string()).await.is_ok() + ); + + } */ + + #[tokio::test] + async fn test_encrypt_decrypt_private_key_with_passphrase() { + let db = get_memory_sqlite_connection().await; + + let plain_text_password = "test".to_string(); + + let secret_message = "trnka".to_string(); + + let application = ApplicationService::create(&db, 103100, &plain_text_password, "".to_string()).await.unwrap(); + + let encrypted_message = + crypto::encrypt_password_with_recipients(&secret_message, &vec![&application.public_key]) + .await + .unwrap(); + + let private_key_plain_text = + crypto::decrypt_password(application.private_key, plain_text_password) + .await + .unwrap(); + + let decrypted_message = + crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text) + .await + .unwrap(); + + assert_eq!(secret_message, decrypted_message); + } } \ No newline at end of file diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index da059f8..c1faca0 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use chrono::Duration; -use entity::{candidate, session}; +use entity::{candidate, session, application}; use sea_orm::{prelude::Uuid, DbConn, IntoActiveModel}; use crate::{ @@ -12,9 +12,6 @@ use crate::{ use super::{session_service::SessionService, portfolio_service::PortfolioService}; - -const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; - pub struct CandidateService; impl CandidateService { @@ -25,104 +22,20 @@ impl CandidateService { /// Public key pub(in crate::services) async fn create( db: &DbConn, - application_id: i32, - plain_text_password: &String, - personal_id_number: String, + enc_personal_id_number: String, ) -> Result { - // Check if application id starts with 101, 102 or 103 - if !CandidateService::is_application_id_valid(application_id) { - return Err(ServiceError::InvalidApplicationId); - } - - // Check if user with that application id already exists - if Query::find_candidate_by_id(db, application_id) - .await? - .is_some() - { - return Err(ServiceError::UserAlreadyExists); - } - PortfolioService::create_user_dir(application_id).await?; - - - let hashed_password = hash_password(plain_text_password.to_string()).await?; - let (pubkey, priv_key_plain_text) = crypto::create_identity(); - let encrypted_priv_key = crypto::encrypt_password( - priv_key_plain_text, - plain_text_password.to_string() - ).await?; - - let recipients = get_recipients(db, &pubkey).await?; - let enc_personal_id_number = EncryptedString::new( - &personal_id_number, - &recipients, - ).await?; - let candidate = Mutation::create_candidate( db, - application_id, - hashed_password, - enc_personal_id_number.to_string(), - pubkey, - encrypted_priv_key, + enc_personal_id_number, ) .await?; + + PortfolioService::create_user_dir(candidate.application).await?; + Ok(candidate) } - pub async fn reset_password( - admin_private_key: String, - db: &DbConn, - id: i32, - ) -> Result { - let candidate = Query::find_candidate_by_id(db, id).await? - .ok_or(ServiceError::CandidateNotFound)?; - let parents = Query::find_candidate_parents(db, &candidate).await?; - - - let new_password_plain = crypto::random_12_char_string(); - let new_password_hash = crypto::hash_password(new_password_plain.clone()).await?; - - let (pubkey, priv_key_plain_text) = crypto::create_identity(); - let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text.clone(), - new_password_plain.to_string() - ).await?; - - - Self::delete_old_sessions(db, &candidate, 0).await?; - let candidate = Mutation::update_candidate_password_and_keys(db, - candidate, - new_password_hash, - pubkey.clone(), - encrypted_priv_key - ).await?; - - - // user might no have filled his details yet, but personal id number is filled from beginning - let personal_id_number = EncryptedString::from(candidate.personal_identification_number.clone()) - .decrypt(&admin_private_key) - .await?; - - let recipients = get_recipients(db, &pubkey).await?; - - let dec_details = EncryptedApplicationDetails::from((&candidate, parents.clone())) - .decrypt(admin_private_key).await?; - let enc_details = EncryptedApplicationDetails::new(&dec_details, recipients).await?; - - Mutation::update_candidate_details(db, candidate, enc_details.candidate).await?; - for i in 0..enc_details.parents.len() { - Mutation::add_parent_details(db, parents[i].clone(), enc_details.parents[i].clone()).await?; - } - - Ok( - CreateCandidateResponse { - application_id: id, - personal_id_number, - password: new_password_plain, - } - ) - } - pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> { PortfolioService::delete_candidate_root(candidate.application).await?; @@ -166,172 +79,23 @@ impl CandidateService { }) ).await } - - async fn decrypt_private_key( - candidate: candidate::Model, - password: String, - ) -> Result { - let private_key_encrypted = candidate.private_key; - - let private_key = crypto::decrypt_password(private_key_encrypted, password).await?; - - Ok(private_key) - } - - fn is_application_id_valid(application_id: i32) -> bool { - let s = &application_id.to_string(); - if s.len() <= 3 { - // TODO: does the field of study prefix have to be exactly 6 digits? VYRESIT PODLE PRIHLASEK!!! - return false; - } - let field_of_study_prefix = &s[0..3]; - FIELD_OF_STUDY_PREFIXES.contains(&field_of_study_prefix) - } - - pub async fn extend_session_duration_to_14_days(db: &DbConn, session: session::Model) -> Result { - let now = chrono::Utc::now().naive_utc(); - if now >= session.updated_at.checked_add_signed(Duration::days(1)).ok_or(ServiceError::Unauthorized)? { - let new_expires_at = now.checked_add_signed(Duration::days(14)).ok_or(ServiceError::Unauthorized)?; - - Ok(Mutation::update_session_expiration(db, session, new_expires_at).await?) - } else { - Ok(session) - } - } -} - -#[async_trait] -impl AuthenticableTrait for CandidateService { - type User = candidate::Model; - type Session = session::Model; - - async fn login( - db: &DbConn, - application_id: i32, - password: String, - ip_addr: String, - ) -> Result<(String, String), ServiceError> { - let candidate = Query::find_candidate_by_id(db, application_id) - .await? - .ok_or(ServiceError::CandidateNotFound)?; - - let session_id = Self::new_session(db, &candidate, password.clone(), ip_addr).await?; - - let private_key = Self::decrypt_private_key(candidate, password).await?; - Ok((session_id, private_key)) - } - - async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { - let session = Query::find_session_by_uuid(db, session_uuid) - .await? - .ok_or(ServiceError::Unauthorized)?; - - if !SessionService::is_valid(&session).await? { - Mutation::delete_session(db, session.into_active_model()).await?; - return Err(ServiceError::ExpiredSession); - } - // Candidate authenticated - - Self::extend_session_duration_to_14_days(db, session.clone()).await?; - - let candidate = Query::find_candidate_by_id(db, session.candidate_id.unwrap()) - .await? - .ok_or(ServiceError::CandidateNotFound)?; - - Ok(candidate) - } - - async fn logout(db: &DbConn, session: session::Model) -> Result<(), ServiceError> { - Mutation::delete_session(db, session.into_active_model()).await?; - Ok(()) - } - - async fn new_session( - db: &DbConn, - candidate: &candidate::Model, - password: String, - ip_addr: String, - ) -> Result { - if !crypto::verify_password(password.clone(), candidate.code.clone()).await? { - return Err(ServiceError::InvalidCredentials); - } - // user is authenticated, generate a new session - let random_uuid: Uuid = Uuid::new_v4(); - - let session = Mutation::insert_candidate_session(db, random_uuid, candidate.application, ip_addr).await?; - - Self::delete_old_sessions(db, &candidate, 3).await?; - - Ok(session.id.to_string()) - } - async fn delete_old_sessions( - db: &DbConn, - candidate: &candidate::Model, - keep_n_recent: usize, - ) -> Result<(), ServiceError> { - let sessions = Query::find_related_candidate_sessions(db, &candidate) - .await? - .iter() - .map(|s| s.to_owned().into_active_model()) - .collect(); - - SessionService::delete_sessions(db, sessions, keep_n_recent).await?; - Ok(()) - } - } #[cfg(test)] pub mod tests { use sea_orm::DbConn; - use crate::models::auth::AuthenticableTrait; use crate::models::candidate_details::tests::assert_all_application_details; use crate::utils::db::get_memory_sqlite_connection; use crate::{crypto, services::candidate_service::CandidateService, Mutation}; use crate::models::candidate_details::EncryptedApplicationDetails; - use entity::{candidate, parent, admin}; + use entity::{application, candidate, parent, admin}; use crate::services::application_service::ApplicationService; const APPLICATION_ID: i32 = 103151; - #[tokio::test] - async fn test_application_id_validation() { - assert!(CandidateService::is_application_id_valid(101_101)); - assert!(CandidateService::is_application_id_valid(102_107)); - assert!(CandidateService::is_application_id_valid(103_109)); - assert!(!CandidateService::is_application_id_valid(104_109)); - assert!(!CandidateService::is_application_id_valid(100_109)); - assert!(!CandidateService::is_application_id_valid(201_109)); - assert!(!CandidateService::is_application_id_valid(101)); - } - - #[tokio::test] - async fn test_password_reset() { - let db = get_memory_sqlite_connection().await; - let admin = create_admin(&db).await; - let (candidate, _parent) = put_user_data(&db).await; - - let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); - - assert!( - CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_ok() - ); - - let new_password = CandidateService::reset_password(private_key, &db, candidate.application).await.unwrap().password; - - assert!( - CandidateService::login(&db, candidate.application, "test".to_string(), "127.0.0.1".to_string()).await.is_err() - ); - - assert!( - CandidateService::login(&db, candidate.application, new_password, "127.0.0.1".to_string()).await.is_ok() - ); - - } - #[tokio::test] async fn test_list_candidates() { let db = get_memory_sqlite_connection().await; @@ -346,39 +110,6 @@ pub mod tests { assert_eq!(candidates.len(), 1); } - #[tokio::test] - async fn test_encrypt_decrypt_private_key_with_passphrase() { - let db = get_memory_sqlite_connection().await; - - let plain_text_password = "test".to_string(); - - let secret_message = "trnka".to_string(); - - let candidate = CandidateService::create(&db, APPLICATION_ID, &plain_text_password, "".to_string()) - .await - .ok() - .unwrap(); - - Mutation::create_parent(&db, APPLICATION_ID).await.unwrap(); - - let encrypted_message = - crypto::encrypt_password_with_recipients(&secret_message, &vec![&candidate.public_key]) - .await - .unwrap(); - - let private_key_plain_text = - crypto::decrypt_password(candidate.private_key, plain_text_password) - .await - .unwrap(); - - let decrypted_message = - crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text) - .await - .unwrap(); - - assert_eq!(secret_message, decrypted_message); - } - #[cfg(test)] async fn create_admin(db: &DbConn) -> admin::Model { use chrono::Utc; @@ -405,27 +136,27 @@ pub mod tests { } #[cfg(test)] - pub async fn put_user_data(db: &DbConn) -> (candidate::Model, Vec) { + pub async fn put_user_data(db: &DbConn) -> (application::Model, candidate::Model, Vec) { use crate::models::candidate_details::tests::APPLICATION_DETAILS; let plain_text_password = "test".to_string(); - let (candidate, _parent) = ApplicationService::create_candidate_with_parent( - &db, + let application = ApplicationService::create( + db, APPLICATION_ID, &plain_text_password, - "".to_string(), - ) - .await - .ok() - .unwrap(); + "0000001111".to_string() + ).await.unwrap(); + + let candidate= ApplicationService::find_related_candidate(db, application.to_owned()).await.unwrap(); let form = APPLICATION_DETAILS.lock().unwrap().clone(); - let (candidate, parents) = ApplicationService::add_all_details(&db, candidate, &form) + let (candidate, parents) = ApplicationService::add_all_details(&db, &application.public_key, candidate, &form) .await .unwrap(); ( + application, candidate, parents, ) @@ -434,7 +165,7 @@ pub mod tests { #[tokio::test] async fn test_put_user_data() { let db = get_memory_sqlite_connection().await; - let (candidate, parents) = put_user_data(&db).await; + let (application, candidate, parents) = put_user_data(&db).await; assert!(candidate.name.is_some()); assert!(parents[0].name.is_some()); } @@ -443,9 +174,9 @@ pub mod tests { async fn test_encrypt_decrypt_user_data() { let password = "test".to_string(); let db = get_memory_sqlite_connection().await; - let (enc_candidate, enc_parent) = put_user_data(&db).await; + let (application, enc_candidate, enc_parent) = put_user_data(&db).await; - let dec_priv_key = crypto::decrypt_password(enc_candidate.private_key.clone(), password) + let dec_priv_key = crypto::decrypt_password(application.private_key.clone(), password) .await .unwrap(); let enc_details = EncryptedApplicationDetails::try_from((&enc_candidate, enc_parent)) diff --git a/core/src/services/parent_service.rs b/core/src/services/parent_service.rs index 2ee3e9b..a8e4825 100644 --- a/core/src/services/parent_service.rs +++ b/core/src/services/parent_service.rs @@ -93,18 +93,19 @@ mod tests { #[tokio::test] async fn create_parent_test() { let db = get_memory_sqlite_connection().await; - CandidateService::create(&db, 103100, &"test".to_string(), "".to_string()).await.unwrap(); - super::ParentService::create(&db, 103100).await.unwrap(); - super::ParentService::create(&db, 103100).await.unwrap(); + let candidate = CandidateService::create(&db, "".to_string()).await.unwrap(); + super::ParentService::create(&db, candidate.application).await.unwrap(); + super::ParentService::create(&db, candidate.application).await.unwrap(); } - #[tokio::test] + /* #[tokio::test] async fn add_parent_details_test() { let db = get_memory_sqlite_connection().await; let plain_text_password = "test".to_string(); - let (candidate, _parent) = ApplicationService::create_candidate_with_parent( + let application = ApplicationService::create(&db, 103151, &plain_text_password, "0000001111".to_string()).await.unwrap(); + let (application, candidate, _parent) = ApplicationService::create_candidate_with_parent( &db, - 103101, + application, &plain_text_password, "".to_string(), ) @@ -116,11 +117,11 @@ mod tests { let form = APPLICATION_DETAILS_TWO_PARENTS.lock().unwrap().clone(); - let (candidate, parents) = ApplicationService::add_all_details(&db, candidate, &form) + let (candidate, parents) = ApplicationService::add_all_details(&db, &application.public_key, candidate, &form) .await .unwrap(); - let priv_key = crypto::decrypt_password(candidate.private_key.clone(), plain_text_password).await.unwrap(); + let priv_key = crypto::decrypt_password(application.private_key.clone(), plain_text_password).await.unwrap(); let dec_details = EncryptedApplicationDetails::try_from((&candidate, parents)) .unwrap() .decrypt(priv_key) @@ -149,5 +150,5 @@ mod tests { - } + } */ } \ No newline at end of file diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index 6ae6f4b..0c50669 100644 --- a/core/src/services/portfolio_service.rs +++ b/core/src/services/portfolio_service.rs @@ -268,7 +268,7 @@ impl PortfolioService { /// Move files from cache to final directory and delete cache afterwards - pub async fn submit(candidate: &candidate::Model, db: &DbConn) -> Result<(), ServiceError> { + pub async fn submit(public_key: &String, candidate: &candidate::Model, db: &DbConn) -> Result<(), ServiceError> { let candidate_id = candidate.application; let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf(); let cache_path = path.join("cache"); @@ -307,9 +307,8 @@ impl PortfolioService { archive.shutdown().await?; let admin_public_keys = Query::get_all_admin_public_keys(db).await?; - let candidate_public_key = &candidate.public_key; let mut admin_public_keys_refrence: Vec<&str> = admin_public_keys.iter().map(|s| &**s).collect(); - let mut recipients = vec![&**candidate_public_key]; + let mut recipients = vec![&**public_key]; recipients.append(&mut admin_public_keys_refrence); let final_path = path.join(FileType::PortfolioZip.as_str()); @@ -420,18 +419,17 @@ mod tests { #[serial] async fn test_folder_creation() { let db = get_memory_sqlite_connection().await; - let plain_text_password = "test".to_string(); let temp_dir = std::env::temp_dir().join("portfolio_test_tempdir").join("create_folder"); std::env::set_var("PORTFOLIO_STORE_PATH", temp_dir.to_str().unwrap()); - CandidateService::create(&db, APPLICATION_ID, &plain_text_password, "".to_string()) + let candidate = CandidateService::create(&db, "".to_string()) .await .ok() .unwrap(); - assert!(tokio::fs::metadata(temp_dir.join(APPLICATION_ID.to_string())).await.is_ok()); - assert!(tokio::fs::metadata(temp_dir.join(APPLICATION_ID.to_string()).join("cache")).await.is_ok()); + assert!(tokio::fs::metadata(temp_dir.join(candidate.application.to_string())).await.is_ok()); + assert!(tokio::fs::metadata(temp_dir.join(candidate.application.to_string()).join("cache")).await.is_ok()); tokio::fs::remove_dir_all(temp_dir).await.unwrap(); } @@ -620,16 +618,16 @@ mod tests { #[tokio::test] #[serial] async fn test_add_portfolio() { - let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await; - let db = get_memory_sqlite_connection().await; - let (candidate, _) = put_user_data(&db).await; + let (application, candidate, _) = put_user_data(&db).await; + + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.application).await; - PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::submit(&candidate, &db).await.unwrap(); + PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); assert!(tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); @@ -639,20 +637,20 @@ mod tests { #[tokio::test] #[serial] async fn test_delete_portfolio() { - let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await; - let db = get_memory_sqlite_connection().await; - let (candidate, _) = put_user_data(&db).await; + let (application, candidate, _) = put_user_data(&db).await; - PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.application).await; - PortfolioService::submit(&candidate, &db).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); + + PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); assert!(tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); - PortfolioService::delete_portfolio(APPLICATION_ID).await.unwrap(); + PortfolioService::delete_portfolio(candidate.application).await.unwrap(); assert!(!tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); @@ -662,32 +660,32 @@ mod tests { #[tokio::test] #[serial] async fn test_is_portfolio_submitted() { - let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; - let db = get_memory_sqlite_connection().await; - let (candidate, _) = put_user_data(&db).await; - PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + let (application, candidate, _) = put_user_data(&db).await; + let (temp_dir, _, _) = create_data_store_temp_dir(candidate.application).await; - PortfolioService::submit(&candidate, &db).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); + + PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); - assert!(PortfolioService::is_portfolio_submitted(APPLICATION_ID).await); + assert!(PortfolioService::is_portfolio_submitted(candidate.application).await); clear_data_store_temp_dir(temp_dir).await; - let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await; + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.application).await; - PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::submit(&candidate, &db).await.unwrap(); + PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); tokio::fs::remove_file(application_dir.join("PORTFOLIO.age")).await.unwrap(); - assert!(!PortfolioService::is_portfolio_submitted(APPLICATION_ID).await); + assert!(!PortfolioService::is_portfolio_submitted(candidate.application).await); clear_data_store_temp_dir(temp_dir).await; } @@ -695,30 +693,30 @@ mod tests { #[tokio::test] #[serial] async fn test_get_portfolio() { - let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; - let db = get_memory_sqlite_connection().await; - let (candidate, _parent) = put_user_data(&db).await; + let (application, candidate, _parent) = put_user_data(&db).await; - let private_key = crypto::decrypt_password(candidate.private_key.clone(), "test".to_string()) + let (temp_dir, _, _) = create_data_store_temp_dir(candidate.application).await; + + let private_key = crypto::decrypt_password(application.private_key.clone(), "test".to_string()) .await .unwrap(); - PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]) + PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]) .await .unwrap(); - PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]) + PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]) .await .unwrap(); - PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]) + PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]) .await .unwrap(); - PortfolioService::submit(&candidate, &db) + PortfolioService::submit(&application.public_key, &candidate, &db) .await .unwrap(); - PortfolioService::get_portfolio(APPLICATION_ID, private_key) + PortfolioService::get_portfolio(candidate.application, private_key) .await .unwrap(); diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index 58cd477..2d69d48 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -42,30 +42,21 @@ mod tests { use crate::{ crypto, - services::{application_service::ApplicationService, candidate_service::CandidateService}, + services::{application_service::ApplicationService}, utils::db::get_memory_sqlite_connection, models::auth::AuthenticableTrait, }; + const SECRET: &str = "Tajny_kod"; #[tokio::test] async fn test_create_candidate() { - const SECRET: &str = "Tajny_kod"; let db = get_memory_sqlite_connection().await; - let candidate = ApplicationService::create_candidate_with_parent( - &db, - 103151, - &SECRET.to_string(), - "".to_string(), - ) - .await - .ok() - .unwrap() - .0; + let application = ApplicationService::create(&db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); - assert_eq!(candidate.application, 103151); - assert_ne!(candidate.code, SECRET.to_string()); - assert!(crypto::verify_password(SECRET.to_string(), candidate.code) + assert_eq!(application.id.to_owned(), 103151); + assert_ne!(application.password.to_owned(), SECRET.to_string()); + assert!(crypto::verify_password(SECRET.to_string(), application.password) .await .ok() .unwrap()); @@ -75,28 +66,20 @@ mod tests { async fn test_candidate_session_correct_password() { let db = &get_memory_sqlite_connection().await; - let candidate = ApplicationService::create_candidate_with_parent( - db, - 103151, - &"Tajny_kod".to_string(), - "".to_string(), - ) - .await - .unwrap() - .0; + let application = ApplicationService::create(&db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); // correct password - let session = CandidateService::new_session( + let session = ApplicationService::new_session( db, - &candidate, - "Tajny_kod".to_string(), + &application, + SECRET.to_string(), "127.0.0.1".to_string(), ) .await .unwrap(); // println!("{}", session.err().unwrap().1); assert!( - CandidateService::auth(db, Uuid::parse_str(&session).unwrap()) + ApplicationService::auth(db, Uuid::parse_str(&session).unwrap()) .await .is_ok() ); @@ -106,20 +89,12 @@ mod tests { async fn test_candidate_session_incorrect_password() { let db = &get_memory_sqlite_connection().await; - let candidate_form = ApplicationService::create_candidate_with_parent( - &db, - 103151, - &"Tajny_kod".to_string(), - "".to_string(), - ) - .await - .unwrap() - .0; + let application = ApplicationService::create(&db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); // incorrect password - assert!(CandidateService::new_session( + assert!(ApplicationService::new_session( db, - &candidate_form, + &application, "Spatny_kod".to_string(), "127.0.0.1".to_string() ) diff --git a/core/src/utils/db.rs b/core/src/utils/db.rs index ac1b4bd..7b7c779 100644 --- a/core/src/utils/db.rs +++ b/core/src/utils/db.rs @@ -1,4 +1,4 @@ -use entity::admin_session; +use entity::{admin_session, application}; use sea_orm::DbConn; use crate::Query; @@ -22,15 +22,17 @@ pub async fn get_memory_sqlite_connection() -> sea_orm::DbConn { 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 stmt2: TableCreateStatement = schema.create_table_from_entity(application::Entity); let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity); - let stmt4: TableCreateStatement = schema.create_table_from_entity(parent::Entity); + let stmt4: TableCreateStatement = schema.create_table_from_entity(admin::Entity); let stmt5: TableCreateStatement = schema.create_table_from_entity(admin_session::Entity); + let stmt6: TableCreateStatement = schema.create_table_from_entity(parent::Entity); db.execute(db.get_database_backend().build(&stmt)).await.unwrap(); db.execute(db.get_database_backend().build(&stmt2)).await.unwrap(); db.execute(db.get_database_backend().build(&stmt3)).await.unwrap(); db.execute(db.get_database_backend().build(&stmt4)).await.unwrap(); db.execute(db.get_database_backend().build(&stmt5)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt6)).await.unwrap(); db } diff --git a/entity/src/application.rs b/entity/src/application.rs new file mode 100644 index 0000000..2067c8c --- /dev/null +++ b/entity/src/application.rs @@ -0,0 +1,45 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "application")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: i32, + pub candidate_id: i32, + pub password: String, + pub public_key: String, + pub private_key: String, + pub personal_id_number: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::candidate::Entity", + from = "Column::CandidateId", + to = "super::candidate::Column::Application", + on_update = "Cascade", + on_delete = "Cascade" + )] + Candidate, + #[sea_orm(has_many = "super::session::Entity")] + Session, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Candidate.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index 5960976..96ccb92 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -5,9 +5,8 @@ use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "candidate")] pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] + #[sea_orm(primary_key)] pub application: i32, - pub code: String, pub name: Option, pub surname: Option, pub birth_surname: Option, @@ -22,23 +21,21 @@ pub struct Model { pub personal_identification_number: String, pub school_name: Option, pub health_insurance: Option, - pub public_key: String, - pub private_key: String, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - #[sea_orm(has_many = "super::session::Entity")] - Session, + #[sea_orm(has_many = "super::application::Entity")] + Application, #[sea_orm(has_many = "super::parent::Entity")] Parent, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Session.def() + Relation::Application.def() } } diff --git a/entity/src/lib.rs b/entity/src/lib.rs index c2e3813..9904381 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -5,4 +5,5 @@ pub mod candidate; pub mod parent; pub mod session; pub mod admin_session; -pub mod session_trait; \ No newline at end of file +pub mod session_trait; +pub mod application; \ No newline at end of file diff --git a/entity/src/mod.rs b/entity/src/mod.rs index 268951e..43ca935 100644 --- a/entity/src/mod.rs +++ b/entity/src/mod.rs @@ -4,6 +4,7 @@ pub mod prelude; pub mod admin; pub mod admin_session; +pub mod application; pub mod candidate; pub mod parent; pub mod session; diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs index 55797e7..c598a98 100644 --- a/entity/src/prelude.rs +++ b/entity/src/prelude.rs @@ -2,6 +2,7 @@ pub use super::admin::Entity as Admin; pub use super::admin_session::Entity as AdminSession; +pub use super::application::Entity as Application; pub use super::candidate::Entity as Candidate; pub use super::parent::Entity as Parent; pub use super::session::Entity as Session; diff --git a/entity/src/session.rs b/entity/src/session.rs index 63efcde..97ddaa8 100644 --- a/entity/src/session.rs +++ b/entity/src/session.rs @@ -19,18 +19,18 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::candidate::Entity", + belongs_to = "super::application::Entity", from = "Column::CandidateId", - to = "super::candidate::Column::Application", + to = "super::application::Column::Id", on_update = "Cascade", on_delete = "Cascade" )] - Candidate, + Application, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Candidate.def() + Relation::Application.def() } } diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 8b2f658..13315cf 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -9,6 +9,8 @@ mod m20221027_194728_session_create_user_fk; mod m20221028_194728_session_create_admin_fk; mod m20221112_112212_create_parent_candidate_fk; mod m20221221_162232_create_admin_session; +mod m20230114_114628_create_application; +mod m20230114_114826_create_application_candidate_fk; pub struct Migrator; #[async_trait::async_trait] @@ -20,6 +22,7 @@ impl MigratorTrait for Migrator { Box::new(m20221024_124701_create_parent::Migration), Box::new(m20221025_154422_create_session::Migration), Box::new(m20221221_162232_create_admin_session::Migration), + Box::new(m20230114_114628_create_application::Migration), ]; if cfg!(debug_assertions) || cfg!(test) { @@ -36,6 +39,7 @@ impl MigratorTrait for Migrator { migrations.push(Box::new( m20221028_194728_session_create_admin_fk::Migration, )); + migrations.push(Box::new(m20230114_114826_create_application_candidate_fk::Migration)); } migrations diff --git a/migration/src/m20221024_121621_create_candidate.rs b/migration/src/m20221024_121621_create_candidate.rs index 5ee04bf..38532b6 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -16,9 +16,8 @@ impl MigrationTrait for Migration { .integer() .not_null() .primary_key() - .unique_key(), + .auto_increment(), ) - .col(ColumnDef::new(Candidate::Code).string().not_null()) .col(ColumnDef::new(Candidate::Name).string()) .col(ColumnDef::new(Candidate::Surname).string()) .col(ColumnDef::new(Candidate::BirthSurname).string()) @@ -33,8 +32,6 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Candidate::PersonalIdentificationNumber).string().not_null()) .col(ColumnDef::new(Candidate::SchoolName).string()) .col(ColumnDef::new(Candidate::HealthInsurance).string()) - .col(ColumnDef::new(Candidate::PublicKey).string().not_null()) - .col(ColumnDef::new(Candidate::PrivateKey).string().not_null()) .col(ColumnDef::new(Candidate::CreatedAt).date_time().not_null()) .col(ColumnDef::new(Candidate::UpdatedAt).date_time().not_null()) .to_owned(), @@ -53,7 +50,6 @@ impl MigrationTrait for Migration { pub enum Candidate { Table, Application, - Code, Name, Surname, BirthSurname, @@ -68,8 +64,6 @@ pub enum Candidate { PersonalIdentificationNumber, SchoolName, HealthInsurance, - PublicKey, - PrivateKey, CreatedAt, UpdatedAt, } diff --git a/migration/src/m20221027_194728_session_create_user_fk.rs b/migration/src/m20221027_194728_session_create_user_fk.rs index 9a06760..309cdcd 100644 --- a/migration/src/m20221027_194728_session_create_user_fk.rs +++ b/migration/src/m20221027_194728_session_create_user_fk.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::{m20221025_154422_create_session::Session, m20221024_121621_create_candidate::Candidate}; +use crate::{m20221025_154422_create_session::Session, m20230114_114628_create_application::Application}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -11,7 +11,7 @@ impl MigrationTrait for Migration { manager.create_foreign_key(ForeignKey::create() .name("user_fk") .from(Session::Table, Session::CandidateId) - .to(Candidate::Table, Candidate::Application) + .to(Application::Table, Application::Id) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade) .to_owned()).await diff --git a/migration/src/m20230114_114628_create_application.rs b/migration/src/m20230114_114628_create_application.rs new file mode 100644 index 0000000..48330ab --- /dev/null +++ b/migration/src/m20230114_114628_create_application.rs @@ -0,0 +1,51 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Application::Table) + .if_not_exists() + .col( + ColumnDef::new(Application::Id) + .integer() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Application::CandidateId).integer().not_null()) + .col(ColumnDef::new(Application::Password).string().not_null()) + .col(ColumnDef::new(Application::PublicKey).string().not_null()) + .col(ColumnDef::new(Application::PrivateKey).string().not_null()) + .col(ColumnDef::new(Application::PersonalIdNumber).string().not_null()) + .col(ColumnDef::new(Application::CreatedAt).date_time().not_null()) + .col(ColumnDef::new(Application::UpdatedAt).date_time().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Application::Table).to_owned()) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +pub enum Application { + Table, + Id, + Password, + PersonalIdNumber, + PublicKey, + PrivateKey, + CandidateId, + CreatedAt, + UpdatedAt, +} diff --git a/migration/src/m20230114_114826_create_application_candidate_fk.rs b/migration/src/m20230114_114826_create_application_candidate_fk.rs new file mode 100644 index 0000000..b92dcf6 --- /dev/null +++ b/migration/src/m20230114_114826_create_application_candidate_fk.rs @@ -0,0 +1,26 @@ +use sea_orm_migration::prelude::*; + +use crate::{m20221024_121621_create_candidate::Candidate, m20230114_114628_create_application::Application}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.create_foreign_key(ForeignKey::create() + .name("candidate_fk") + .from(Application::Table, Application::CandidateId) + .to(Candidate::Table, Candidate::Application) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + .to_owned()).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.drop_foreign_key(ForeignKey::drop() + .name("candidate_fk") + .table(Application::Table) + .to_owned()).await + } +} From 61facc503cbd7fe78c552709965aef5fe01f6f59 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 15:16:44 +0100 Subject: [PATCH 03/46] fix: list candidates --- api/src/routes/admin.rs | 10 ++-- api/src/routes/candidate.rs | 35 ++++++------ core/src/database/mutation/candidate.rs | 10 ++-- core/src/database/mutation/parent.rs | 4 +- core/src/database/query/application.rs | 32 ++++++++++- core/src/database/query/candidate.rs | 12 ++-- core/src/database/query/parent.rs | 2 +- core/src/models/application.rs | 42 ++++++++++++++ core/src/models/candidate.rs | 2 +- core/src/models/mod.rs | 3 +- core/src/services/application_service.rs | 29 ++++++++-- core/src/services/candidate_service.rs | 4 +- core/src/services/parent_service.rs | 6 +- core/src/services/portfolio_service.rs | 56 +++++++++---------- core/src/utils/csv.rs | 2 +- entity/src/application.rs | 2 +- entity/src/candidate.rs | 2 +- entity/src/parent.rs | 2 +- .../src/m20221024_121621_create_candidate.rs | 4 +- ...21112_112212_create_parent_candidate_fk.rs | 2 +- ..._114826_create_application_candidate_fk.rs | 2 +- 21 files changed, 178 insertions(+), 85 deletions(-) create mode 100644 core/src/models/application.rs diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index f297399..0b9d33a 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -2,7 +2,7 @@ use std::net::{SocketAddr, IpAddr, Ipv4Addr}; use portfolio_core::{ crypto::random_12_char_string, - services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, auth::AuthenticableTrait}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv, + services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, auth::AuthenticableTrait, application::ApplicationResponse}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv, }; use requests::{AdminLoginRequest, RegisterRequest}; use rocket::http::{Cookie, Status, CookieJar}; @@ -114,7 +114,7 @@ pub async fn list_candidates( session: AdminAuth, field: Option, page: Option, -) -> Result>, Custom> { +) -> Result>, Custom> { let db = conn.into_inner(); let private_key = session.get_private_key(); if let Some(field) = field.clone() { @@ -124,9 +124,11 @@ pub async fn list_candidates( } - let candidates = CandidateService::list_candidates(&private_key, db, field, page) + /* let candidates = CandidateService::list_candidates(&private_key, db, field, page) .await - .map_err(to_custom_error)?; + .map_err(to_custom_error)?; */ + let candidates = ApplicationService::list_applications(&private_key, db) + .await.map_err(to_custom_error)?; Ok( Json(candidates) diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index 7a01ce7..dbe69c2 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -6,7 +6,6 @@ use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::models::candidate::{ApplicationDetails, NewCandidateResponse}; use portfolio_core::sea_orm::prelude::Uuid; use portfolio_core::services::application_service::ApplicationService; -use portfolio_core::services::candidate_service::CandidateService; use portfolio_core::services::portfolio_service::{PortfolioService, SubmissionProgress}; use requests::LoginRequest; use rocket::http::{Cookie, CookieJar, Status}; @@ -127,9 +126,9 @@ pub async fn upload_cover_letter( session: ApplicationAuth, letter: Letter, ) -> Result<(), Custom> { - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - PortfolioService::add_cover_letter_to_cache(candidate.id, letter.into()) + PortfolioService::add_cover_letter_to_cache(application.candidate_id, letter.into()) .await .map_err(to_custom_error)?; @@ -138,9 +137,9 @@ pub async fn upload_cover_letter( #[delete("/cover_letter")] pub async fn delete_cover_letter(session: ApplicationAuth) -> Result<(), Custom> { - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - PortfolioService::delete_cover_letter_from_cache(candidate.id) + PortfolioService::delete_cover_letter_from_cache(application.candidate_id) .await .map_err(to_custom_error)?; @@ -152,9 +151,9 @@ pub async fn upload_portfolio_letter( session: ApplicationAuth, letter: Letter, ) -> Result<(), Custom> { - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - PortfolioService::add_portfolio_letter_to_cache(candidate.id, letter.into()) + PortfolioService::add_portfolio_letter_to_cache(application.candidate_id, letter.into()) .await .map_err(to_custom_error)?; @@ -165,7 +164,7 @@ pub async fn upload_portfolio_letter( pub async fn delete_portfolio_letter(session: ApplicationAuth) -> Result<(), Custom> { let candidate: entity::application::Model = session.into(); - PortfolioService::delete_portfolio_letter_from_cache(candidate.id) + PortfolioService::delete_portfolio_letter_from_cache(candidate.candidate_id) .await .map_err(to_custom_error)?; @@ -177,9 +176,9 @@ pub async fn upload_portfolio_zip( session: ApplicationAuth, portfolio: Portfolio, ) -> Result<(), Custom> { - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - PortfolioService::add_portfolio_zip_to_cache(candidate.id, portfolio.into()) + PortfolioService::add_portfolio_zip_to_cache(application.candidate_id, portfolio.into()) .await .map_err(to_custom_error)?; @@ -188,9 +187,9 @@ pub async fn upload_portfolio_zip( #[delete("/portfolio_zip")] pub async fn delete_portfolio_zip(session: ApplicationAuth) -> Result<(), Custom> { - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - PortfolioService::delete_portfolio_zip_from_cache(candidate.id) + PortfolioService::delete_portfolio_zip_from_cache(application.candidate_id) .await .map_err(to_custom_error)?; @@ -201,9 +200,9 @@ pub async fn delete_portfolio_zip(session: ApplicationAuth) -> Result<(), Custom pub async fn submission_progress( session: ApplicationAuth, ) -> Result, Custom> { - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - let progress = PortfolioService::get_submission_progress(candidate.id) + let progress = PortfolioService::get_submission_progress(application.candidate_id) .await .map(|x| Json(x)) .map_err(to_custom_error); @@ -242,9 +241,9 @@ pub async fn submit_portfolio( pub async fn delete_portfolio( session: ApplicationAuth, ) -> Result<(), Custom> { - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - PortfolioService::delete_portfolio(candidate.id) + PortfolioService::delete_portfolio(application.candidate_id) .await .map_err(to_custom_error)?; @@ -254,9 +253,9 @@ pub async fn delete_portfolio( #[get("/download")] pub async fn download_portfolio(session: ApplicationAuth) -> Result, Custom> { let private_key = session.get_private_key(); - let candidate: entity::application::Model = session.into(); + let application: entity::application::Model = session.into(); - let file = PortfolioService::get_portfolio(candidate.id, private_key) + let file = PortfolioService::get_portfolio(application.candidate_id, private_key) .await .map_err(to_custom_error); diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 77511b4..9f1be29 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -18,7 +18,7 @@ impl Mutation { .insert(db) .await?; - info!("CANDIDATE {} CREATED", candidate.application); + info!("CANDIDATE {} CREATED", candidate.id); Ok(candidate) } @@ -35,7 +35,7 @@ impl Mutation { db: &DbConn, candidate: candidate::Model, ) -> Result { - let application = candidate.application; + let application = candidate.id; let delete = candidate.delete(db).await?; warn!("CANDIDATE {} DELETED", application); @@ -47,7 +47,7 @@ impl Mutation { user: candidate::Model, enc_candidate: EncryptedCandidateDetails, ) -> Result { - let application = user.application; + let application = user.id; let mut candidate: candidate::ActiveModel = user.into(); candidate.name = Set(enc_candidate.name.map(|e| e.into())); @@ -92,7 +92,7 @@ mod tests { .await .unwrap(); - let candidate = Query::find_candidate_by_id(&db, candidate.application) + let candidate = Query::find_candidate_by_id(&db, candidate.id) .await .unwrap(); assert!(candidate.is_some()); @@ -116,7 +116,7 @@ mod tests { let candidate = Mutation::update_candidate_details(&db, candidate, encrypted_details.candidate).await.unwrap(); - let candidate = Query::find_candidate_by_id(&db, candidate.application) + let candidate = Query::find_candidate_by_id(&db, candidate.id) .await .unwrap().unwrap(); diff --git a/core/src/database/mutation/parent.rs b/core/src/database/mutation/parent.rs index ccf308e..f3e7ba7 100644 --- a/core/src/database/mutation/parent.rs +++ b/core/src/database/mutation/parent.rs @@ -56,7 +56,7 @@ mod tests { .await .unwrap(); - let new_parent = Mutation::create_parent(&db, candidate.application).await.unwrap(); + let new_parent = Mutation::create_parent(&db, candidate.id).await.unwrap(); let parent = Query::find_parent_by_id(&db, new_parent.id).await.unwrap(); assert!(parent.is_some()); @@ -73,7 +73,7 @@ mod tests { .await .unwrap(); - let parent = Mutation::create_parent(&db, candidate.application).await.unwrap(); + let parent = Mutation::create_parent(&db, candidate.id).await.unwrap(); let encrypted_details: EncryptedApplicationDetails = EncryptedApplicationDetails::new( &APPLICATION_DETAILS.lock().unwrap().clone(), diff --git a/core/src/database/query/application.rs b/core/src/database/query/application.rs index dda7f46..fa37d62 100644 --- a/core/src/database/query/application.rs +++ b/core/src/database/query/application.rs @@ -1,5 +1,17 @@ use entity::{application, candidate}; -use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait}; +use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait}; + +#[derive(FromQueryResult, Clone)] +pub struct ApplicationCandidateJoin { + pub application_id: i32, + // pub personal_id_number: String, + pub candidate_id: i32, + pub name: Option, + pub surname: Option, + pub email: Option, + pub telephone: Option, + pub study: Option, +} use crate::Query; @@ -22,4 +34,22 @@ impl Query { .one(db) .await } + + pub async fn list_applications( + db: &DbConn, + ) -> Result, DbErr> { + application::Entity::find() + // .column_as(application::Column::Id, "application_id") + .join(JoinType::InnerJoin, application::Relation::Candidate.def()) + .column_as(application::Column::Id, "application_id") + .column_as(candidate::Column::Id, "candidate_id") + .column_as(candidate::Column::Name, "name") + .column_as(candidate::Column::Surname, "surname") + .column_as(candidate::Column::Email, "email") + .column_as(candidate::Column::Telephone, "telephone") + .column_as(candidate::Column::Study, "study") + .into_model::() + .all(db) + .await + } } \ No newline at end of file diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index feb8ec8..c2ebc40 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -49,7 +49,7 @@ impl Query { } else { select } - .order_by(candidate::Column::Application, Order::Asc) + .order_by(candidate::Column::Id, Order::Asc) .into_model::(); if let Some(page) = page { @@ -66,7 +66,7 @@ impl Query { db: &DbConn ) -> Result, DbErr> { Candidate::find() - .order_by(candidate::Column::Application, Order::Asc) + .order_by(candidate::Column::Id, Order::Asc) .all(db) .await } @@ -75,8 +75,8 @@ impl Query { db: &DbConn, ) -> Result, DbErr> { Candidate::find() - .order_by(candidate::Column::Application, Order::Asc) - .column(candidate::Column::Application) + .order_by(candidate::Column::Id, Order::Asc) + .column(candidate::Column::Id) .into_model::() .all(db) .await @@ -97,7 +97,7 @@ mod tests { async fn test_find_candidate_by_id() { let db = get_memory_sqlite_connection().await; let candidate = candidate::ActiveModel { - application: Set(103158), + id: Set(103158), personal_identification_number: Set("test".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), updated_at: Set(chrono::offset::Local::now().naive_local()), @@ -107,7 +107,7 @@ mod tests { .await .unwrap(); - let candidate = Query::find_candidate_by_id(&db, candidate.application) + let candidate = Query::find_candidate_by_id(&db, candidate.id) .await .unwrap(); assert!(candidate.is_some()); diff --git a/core/src/database/query/parent.rs b/core/src/database/query/parent.rs index 72a8b19..3e95a3e 100644 --- a/core/src/database/query/parent.rs +++ b/core/src/database/query/parent.rs @@ -45,7 +45,7 @@ mod tests { const APPLICATION_ID: i32 = 103158; candidate::ActiveModel { - application: Set(APPLICATION_ID), + id: Set(APPLICATION_ID), personal_identification_number: Set("test".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), updated_at: Set(chrono::offset::Local::now().naive_local()), diff --git a/core/src/models/application.rs b/core/src/models/application.rs new file mode 100644 index 0000000..3ca4da8 --- /dev/null +++ b/core/src/models/application.rs @@ -0,0 +1,42 @@ +use serde::{Serialize, Deserialize}; + +use crate::{database::query::application::ApplicationCandidateJoin, services::portfolio_service::SubmissionProgress, error::ServiceError}; + +use super::candidate_details::EncryptedString; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplicationResponse { + pub application_id: i32, + // pub personal_id_number: String, + pub candidate_id: i32, + pub name: String, + pub surname: String, + pub email: String, + pub telephone: String, + pub study: String, +} + +impl ApplicationResponse { + pub async fn from_encrypted( + private_key: &String, + c: ApplicationCandidateJoin + ) -> Result { + let name = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.name).ok(), private_key).await?; + let surname = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.surname).ok(), private_key).await?; + let email = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.email).ok(), private_key).await?; + let telephone = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.telephone).ok(), private_key).await?; + + Ok( + Self { + application_id: c.application_id, + name: name.unwrap_or_default(), + surname: surname.unwrap_or_default(), + email: email.unwrap_or_default(), + telephone: telephone.unwrap_or_default(), + study: c.study.unwrap_or_default(), + candidate_id: c.candidate_id, + } + ) + } +} \ No newline at end of file diff --git a/core/src/models/candidate.rs b/core/src/models/candidate.rs index b121094..9a72cc7 100644 --- a/core/src/models/candidate.rs +++ b/core/src/models/candidate.rs @@ -108,7 +108,7 @@ impl NewCandidateResponse { let id_number = EncryptedString::from(c.personal_identification_number).decrypt(private_key).await?; Ok( Self { - application_id: c.application, + application_id: c.id, personal_id_number: id_number, } ) diff --git a/core/src/models/mod.rs b/core/src/models/mod.rs index 4c3b0be..1251244 100644 --- a/core/src/models/mod.rs +++ b/core/src/models/mod.rs @@ -1,3 +1,4 @@ pub mod candidate_details; pub mod candidate; -pub mod auth; \ No newline at end of file +pub mod auth; +pub mod application; \ No newline at end of file diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 43cd54e..42a1ad6 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -3,9 +3,9 @@ use chrono::Duration; use entity::{candidate, parent, application, session}; use sea_orm::{DbConn, prelude::Uuid, IntoActiveModel}; -use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::{candidate::ApplicationDetails, candidate_details::EncryptedString, auth::AuthenticableTrait}, Mutation, crypto::{hash_password, self}}; +use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::{candidate::ApplicationDetails, candidate_details::EncryptedString, auth::AuthenticableTrait, application::ApplicationResponse}, Mutation, crypto::{hash_password, self}}; -use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService}; +use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService, portfolio_service::PortfolioService}; const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; @@ -60,7 +60,7 @@ impl ApplicationService { let application = Mutation::create_application( db, application_id, - candidate.application, + candidate.id, hashed_password, enc_personal_id_number.to_string(), pubkey, @@ -87,8 +87,8 @@ impl ApplicationService { personal_id_number: String, ) -> Result<(application::Model, candidate::Model, parent::Model), ServiceError> { let candidate = CandidateService::create(db, personal_id_number).await?; - let parent = ParentService::create(db, candidate.application).await?; - let application = Mutation::update_candidate_fk(db, application, candidate.application).await?; + let parent = ParentService::create(db, candidate.id).await?; + let application = Mutation::update_candidate_fk(db, application, candidate.id).await?; Ok( ( application, @@ -141,7 +141,26 @@ impl ApplicationService { } else { Err(ServiceError::Forbidden) } + } + pub async fn list_applications( + private_key: &String, + db: &DbConn, + ) -> Result, ServiceError> { + let applications = Query::list_applications(db).await?; + + futures::future::try_join_all( + applications + .iter() + .map(|c| async move { + ApplicationResponse::from_encrypted( + private_key, + c.to_owned() + ).await + }) + ).await + + } async fn decrypt_private_key( diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index c1faca0..029591c 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -30,14 +30,14 @@ impl CandidateService { ) .await?; - PortfolioService::create_user_dir(candidate.application).await?; + PortfolioService::create_user_dir(candidate.id).await?; Ok(candidate) } pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> { - PortfolioService::delete_candidate_root(candidate.application).await?; + PortfolioService::delete_candidate_root(candidate.id).await?; Mutation::delete_candidate(db, candidate).await?; Ok(()) diff --git a/core/src/services/parent_service.rs b/core/src/services/parent_service.rs index a8e4825..14655df 100644 --- a/core/src/services/parent_service.rs +++ b/core/src/services/parent_service.rs @@ -32,7 +32,7 @@ impl ParentService { for i in 0..parents_details.len() { let found_parent = match found_parents.get(i) { Some(parent) => parent.to_owned(), - None => ParentService::create(db, ref_candidate.application).await?, + None => ParentService::create(db, ref_candidate.id).await?, }; let enc_details = EncryptedParentDetails::new(&parents_details[i], recipients).await?; let parent = Mutation::add_parent_details(db, found_parent, enc_details.clone()).await?; @@ -94,8 +94,8 @@ mod tests { async fn create_parent_test() { let db = get_memory_sqlite_connection().await; let candidate = CandidateService::create(&db, "".to_string()).await.unwrap(); - super::ParentService::create(&db, candidate.application).await.unwrap(); - super::ParentService::create(&db, candidate.application).await.unwrap(); + super::ParentService::create(&db, candidate.id).await.unwrap(); + super::ParentService::create(&db, candidate.id).await.unwrap(); } /* #[tokio::test] diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index 0c50669..ec0dc05 100644 --- a/core/src/services/portfolio_service.rs +++ b/core/src/services/portfolio_service.rs @@ -269,7 +269,7 @@ impl PortfolioService { /// Move files from cache to final directory and delete cache afterwards pub async fn submit(public_key: &String, candidate: &candidate::Model, db: &DbConn) -> Result<(), ServiceError> { - let candidate_id = candidate.application; + let candidate_id = candidate.id; let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf(); let cache_path = path.join("cache"); @@ -277,7 +277,7 @@ impl PortfolioService { return Err(ServiceError::IncompletePortfolio); } - info!("PORTFOLIO {} SUBMIT STARTED", candidate.application); + info!("PORTFOLIO {} SUBMIT STARTED", candidate.id); let mut archive = tokio::fs::File::create(path.join(FileType::PortfolioZip.as_str())).await?; let mut writer = async_zip::write::ZipFileWriter::new(&mut archive); @@ -428,8 +428,8 @@ mod tests { .ok() .unwrap(); - assert!(tokio::fs::metadata(temp_dir.join(candidate.application.to_string())).await.is_ok()); - assert!(tokio::fs::metadata(temp_dir.join(candidate.application.to_string()).join("cache")).await.is_ok()); + assert!(tokio::fs::metadata(temp_dir.join(candidate.id.to_string())).await.is_ok()); + assert!(tokio::fs::metadata(temp_dir.join(candidate.id.to_string()).join("cache")).await.is_ok()); tokio::fs::remove_dir_all(temp_dir).await.unwrap(); } @@ -621,11 +621,11 @@ mod tests { let db = get_memory_sqlite_connection().await; let (application, candidate, _) = put_user_data(&db).await; - let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.application).await; + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).await; - PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap(); PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); @@ -640,17 +640,17 @@ mod tests { let db = get_memory_sqlite_connection().await; let (application, candidate, _) = put_user_data(&db).await; - let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.application).await; + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).await; - PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap(); PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); assert!(tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); - PortfolioService::delete_portfolio(candidate.application).await.unwrap(); + PortfolioService::delete_portfolio(candidate.id).await.unwrap(); assert!(!tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); @@ -663,29 +663,29 @@ mod tests { let db = get_memory_sqlite_connection().await; let (application, candidate, _) = put_user_data(&db).await; - let (temp_dir, _, _) = create_data_store_temp_dir(candidate.application).await; + let (temp_dir, _, _) = create_data_store_temp_dir(candidate.id).await; - PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap(); PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); - assert!(PortfolioService::is_portfolio_submitted(candidate.application).await); + assert!(PortfolioService::is_portfolio_submitted(candidate.id).await); clear_data_store_temp_dir(temp_dir).await; - let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.application).await; + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).await; - PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]).await.unwrap(); - PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]).await.unwrap(); + PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]).await.unwrap(); + PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]).await.unwrap(); PortfolioService::submit(&application.public_key, &candidate, &db).await.unwrap(); tokio::fs::remove_file(application_dir.join("PORTFOLIO.age")).await.unwrap(); - assert!(!PortfolioService::is_portfolio_submitted(candidate.application).await); + assert!(!PortfolioService::is_portfolio_submitted(candidate.id).await); clear_data_store_temp_dir(temp_dir).await; } @@ -696,19 +696,19 @@ mod tests { let db = get_memory_sqlite_connection().await; let (application, candidate, _parent) = put_user_data(&db).await; - let (temp_dir, _, _) = create_data_store_temp_dir(candidate.application).await; + let (temp_dir, _, _) = create_data_store_temp_dir(candidate.id).await; let private_key = crypto::decrypt_password(application.private_key.clone(), "test".to_string()) .await .unwrap(); - PortfolioService::add_cover_letter_to_cache(candidate.application, vec![0]) + PortfolioService::add_cover_letter_to_cache(candidate.id, vec![0]) .await .unwrap(); - PortfolioService::add_portfolio_letter_to_cache(candidate.application, vec![0]) + PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]) .await .unwrap(); - PortfolioService::add_portfolio_zip_to_cache(candidate.application, vec![0]) + PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]) .await .unwrap(); @@ -716,7 +716,7 @@ mod tests { .await .unwrap(); - PortfolioService::get_portfolio(candidate.application, private_key) + PortfolioService::get_portfolio(candidate.id, private_key) .await .unwrap(); diff --git a/core/src/utils/csv.rs b/core/src/utils/csv.rs index 6d6cef9..6c3a3b2 100644 --- a/core/src/utils/csv.rs +++ b/core/src/utils/csv.rs @@ -41,7 +41,7 @@ pub async fn export( let candidates_with_parents = Query::list_candidates_full(&db).await?; for candidate in candidates_with_parents { - let application = candidate.application; + let application = candidate.id; let parents = Query::find_candidate_parents(db, &candidate).await?; let row: Row = match EncryptedApplicationDetails::try_from((&candidate, parents)) { diff --git a/entity/src/application.rs b/entity/src/application.rs index 2067c8c..6f82e4b 100644 --- a/entity/src/application.rs +++ b/entity/src/application.rs @@ -21,7 +21,7 @@ pub enum Relation { #[sea_orm( belongs_to = "super::candidate::Entity", from = "Column::CandidateId", - to = "super::candidate::Column::Application", + to = "super::candidate::Column::Id", on_update = "Cascade", on_delete = "Cascade" )] diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index 96ccb92..79f96bc 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "candidate")] pub struct Model { #[sea_orm(primary_key)] - pub application: i32, + pub id: i32, pub name: Option, pub surname: Option, pub birth_surname: Option, diff --git a/entity/src/parent.rs b/entity/src/parent.rs index 1ac42c0..0c84916 100644 --- a/entity/src/parent.rs +++ b/entity/src/parent.rs @@ -21,7 +21,7 @@ pub enum Relation { #[sea_orm( belongs_to = "super::candidate::Entity", from = "Column::Application", - to = "super::candidate::Column::Application", + to = "super::candidate::Column::Id", on_update = "Cascade", on_delete = "Cascade" )] diff --git a/migration/src/m20221024_121621_create_candidate.rs b/migration/src/m20221024_121621_create_candidate.rs index 38532b6..c822cd4 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -12,7 +12,7 @@ impl MigrationTrait for Migration { .table(Candidate::Table) .if_not_exists() .col( - ColumnDef::new(Candidate::Application) + ColumnDef::new(Candidate::Id) .integer() .not_null() .primary_key() @@ -49,7 +49,7 @@ impl MigrationTrait for Migration { #[derive(Iden)] pub enum Candidate { Table, - Application, + Id, Name, Surname, BirthSurname, diff --git a/migration/src/m20221112_112212_create_parent_candidate_fk.rs b/migration/src/m20221112_112212_create_parent_candidate_fk.rs index c390fa0..a2376cf 100644 --- a/migration/src/m20221112_112212_create_parent_candidate_fk.rs +++ b/migration/src/m20221112_112212_create_parent_candidate_fk.rs @@ -11,7 +11,7 @@ impl MigrationTrait for Migration { manager.create_foreign_key(ForeignKey::create() .name("candidate_fk") .from(Parent::Table, Parent::Application) - .to(Candidate::Table, Candidate::Application) + .to(Candidate::Table, Candidate::Id) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade) .to_owned()).await diff --git a/migration/src/m20230114_114826_create_application_candidate_fk.rs b/migration/src/m20230114_114826_create_application_candidate_fk.rs index b92dcf6..b3bbd9f 100644 --- a/migration/src/m20230114_114826_create_application_candidate_fk.rs +++ b/migration/src/m20230114_114826_create_application_candidate_fk.rs @@ -11,7 +11,7 @@ impl MigrationTrait for Migration { manager.create_foreign_key(ForeignKey::create() .name("candidate_fk") .from(Application::Table, Application::CandidateId) - .to(Candidate::Table, Candidate::Application) + .to(Candidate::Table, Candidate::Id) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade) .to_owned()).await From 6e1c35f72174077d9c197ed29b0dde11efdccc39 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 16:27:07 +0100 Subject: [PATCH 04/46] feat: link candidates --- api/src/routes/admin.rs | 23 +++++++--- api/src/test.rs | 1 + core/src/database/mutation/application.rs | 2 +- core/src/database/mutation/candidate.rs | 14 +++++++ core/src/database/query/application.rs | 14 ++++++- core/src/database/query/candidate.rs | 17 +++++++- core/src/services/application_service.rs | 51 +++++++++++++++++++---- core/src/services/candidate_service.rs | 1 + core/src/services/session_service.rs | 6 +-- 9 files changed, 110 insertions(+), 19 deletions(-) diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 0b9d33a..b92a97f 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -85,15 +85,16 @@ pub async fn hello(_session: AdminAuth) -> Result> { #[post("/create", data = "")] pub async fn create_candidate( conn: Connection<'_, Db>, - _session: AdminAuth, + session: AdminAuth, request: Json, ) -> Result, Custom> { let db = conn.into_inner(); let form = request.into_inner(); + let private_key = session.get_private_key(); let plain_text_password = random_12_char_string(); - let application = ApplicationService::create(&db, form.application_id, &plain_text_password, form.personal_id_number.clone()) + let application = ApplicationService::create(&private_key, &db, form.application_id, &plain_text_password, form.personal_id_number.clone()) .await .map_err(to_custom_error)?; @@ -161,10 +162,12 @@ pub async fn get_candidate( let db = conn.into_inner(); let private_key = session.get_private_key(); - let candidate = Query::find_candidate_by_id(db, id) + let application = Query::find_application_by_id(db, id) .await .map_err(|e| to_custom_error(ServiceError::DbError(e)))? .ok_or(to_custom_error(ServiceError::CandidateNotFound))?; + let candidate = ApplicationService::find_related_candidate(db, application).await + .map_err(to_custom_error)?; let details = ApplicationService::decrypt_all_details( private_key, @@ -187,10 +190,13 @@ pub async fn delete_candidate( ) -> Result<(), Custom> { let db = conn.into_inner(); - let candidate = Query::find_candidate_by_id(db, id) + let application = Query::find_application_by_id(db, id) .await .map_err(|e| to_custom_error(ServiceError::DbError(e)))? .ok_or(to_custom_error(ServiceError::CandidateNotFound))?; + let candidate = ApplicationService::find_related_candidate(db, application).await.map_err(to_custom_error)?; + + // TODO CandidateService::delete_candidate(db, candidate) .await @@ -219,12 +225,19 @@ pub async fn reset_candidate_password( #[get("/candidate//portfolio")] pub async fn get_candidate_portfolio( + conn: Connection<'_, Db>, session: AdminAuth, id: i32, ) -> Result, Custom> { + let db = conn.into_inner(); let private_key = session.get_private_key(); - let portfolio = PortfolioService::get_portfolio(id, private_key) + let application = Query::find_application_by_id(db, id) + .await + .map_err(|e| to_custom_error(ServiceError::DbError(e)))? + .ok_or(to_custom_error(ServiceError::CandidateNotFound))?; + + let portfolio = PortfolioService::get_portfolio(application.candidate_id, private_key) .await .map_err(to_custom_error)?; diff --git a/api/src/test.rs b/api/src/test.rs index 98d2ad9..7f38af9 100644 --- a/api/src/test.rs +++ b/api/src/test.rs @@ -43,6 +43,7 @@ pub mod tests { .unwrap(); let application = ApplicationService::create( + &"".to_string(), db, APPLICATION_ID, &CANDIDATE_PASSWORD.to_string(), diff --git a/core/src/database/mutation/application.rs b/core/src/database/mutation/application.rs index e39fcbb..1589c07 100644 --- a/core/src/database/mutation/application.rs +++ b/core/src/database/mutation/application.rs @@ -1,6 +1,6 @@ use ::entity::application; use log::{info, warn}; -use sea_orm::{DbConn, DbErr, Set, ActiveModelTrait, EntityTrait, IntoActiveModel}; +use sea_orm::{DbConn, DbErr, Set, ActiveModelTrait, EntityTrait, IntoActiveModel, QueryFilter, ColumnTrait}; use crate::Mutation; diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 9f1be29..2f964a8 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -72,6 +72,20 @@ impl Mutation { Ok(update) } + + pub async fn update_personal_id( + db: &DbConn, + candidate: candidate::Model, + personal_id: &str, + ) -> Result { + let mut candidate = candidate.into_active_model(); + candidate.personal_identification_number = Set(personal_id.to_string()); + + candidate + .update(db) + .await + + } } #[cfg(test)] diff --git a/core/src/database/query/application.rs b/core/src/database/query/application.rs index fa37d62..54974f7 100644 --- a/core/src/database/query/application.rs +++ b/core/src/database/query/application.rs @@ -1,5 +1,5 @@ use entity::{application, candidate}; -use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait}; +use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait, QueryFilter, ColumnTrait}; #[derive(FromQueryResult, Clone)] pub struct ApplicationCandidateJoin { @@ -52,4 +52,16 @@ impl Query { .all(db) .await } + + pub async fn find_applications_by_candidate_id( + db: &DbConn, + candidate_id: i32, + ) -> Result, DbErr> { + let applications = application::Entity::find() + .filter(application::Column::CandidateId.eq(candidate_id)) + .all(db) + .await?; + + Ok(applications) + } } \ No newline at end of file diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index c2ebc40..6f32d21 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -6,6 +6,12 @@ use crate::Query; pub const PAGE_SIZE: u64 = 20; +#[derive(FromQueryResult)] +pub struct IdPersonalIdNumberJoin { + pub id: i32, + pub personal_id_number: String, +} + #[derive(FromQueryResult)] pub struct ApplicationId { application: i32, @@ -81,7 +87,16 @@ impl Query { .all(db) .await } - + + pub async fn find_candidate_by_personal_id( + db: &DbConn, + personal_id: &str, + ) -> Result, DbErr> { + Candidate::find() + .filter(candidate::Column::PersonalIdentificationNumber.eq(personal_id)) + .one(db) + .await + } } #[cfg(test)] diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 42a1ad6..969c104 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -18,6 +18,7 @@ impl ApplicationService { /// Encrypted private key /// Public key pub async fn create( + admin_private_key: &String, db: &DbConn, application_id: i32, plain_text_password: &String, @@ -35,7 +36,6 @@ impl ApplicationService { { return Err(ServiceError::UserAlreadyExists); } - let hashed_password = hash_password(plain_text_password.to_string()).await?; let (pubkey, priv_key_plain_text) = crypto::create_identity(); @@ -43,20 +43,20 @@ impl ApplicationService { priv_key_plain_text, plain_text_password.to_string() ).await?; - + let recipients = get_recipients(db, &pubkey).await?; let enc_personal_id_number = EncryptedString::new( &personal_id_number, &recipients, ).await?; - - let candidate = CandidateService::create( + + let candidate = Self::find_or_create_candidate_with_personal_id( + admin_private_key, db, - enc_personal_id_number.clone().to_string() + personal_id_number, + &enc_personal_id_number, ).await?; - println!("candidate: {:?}", candidate); - let application = Mutation::create_application( db, application_id, @@ -70,6 +70,41 @@ impl ApplicationService { Ok(application) } + async fn find_or_create_candidate_with_personal_id( + admin_private_key: &String, + db: &DbConn, + personal_id_number: String, + enc_personal_id_number: &EncryptedString, + ) -> Result { + let candidates = Query::list_candidates_full(db).await?; + let ids_decrypted = futures::future::join_all( + candidates.iter().map(|c| async {( + c.id, + EncryptedString::from(c.personal_identification_number.clone()) + .decrypt(admin_private_key) + .await + .unwrap_or_default(), + )} + )) + .await; + + let found_ids: Vec<&(i32, String)> = ids_decrypted + .iter() + .filter(|(_, id)| id == &personal_id_number) + .collect(); + // TODO: take the candidate id directly from the iterator + if found_ids.iter().any(|(_, personal_id)| personal_id == &personal_id_number) { + let candidate = Query::find_candidate_by_id(db, found_ids[0].0) + .await? + .ok_or(ServiceError::CandidateNotFound)?; + let candidate = Mutation::update_personal_id(db, candidate, &enc_personal_id_number.to_owned().to_string()).await?; + println!("Candidates linked!"); + Ok(candidate) + } else { + CandidateService::create(db, enc_personal_id_number.to_owned().to_string()).await + } + } + fn is_application_id_valid(application_id: i32) -> bool { let s = &application_id.to_string(); if s.len() <= 3 { @@ -368,7 +403,7 @@ mod tests { let secret_message = "trnka".to_string(); - let application = ApplicationService::create(&db, 103100, &plain_text_password, "".to_string()).await.unwrap(); + let application = ApplicationService::create(&"".to_string(), &db, 103100, &plain_text_password, "".to_string()).await.unwrap(); let encrypted_message = crypto::encrypt_password_with_recipients(&secret_message, &vec![&application.public_key]) diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 029591c..17baa49 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -141,6 +141,7 @@ pub mod tests { let plain_text_password = "test".to_string(); let application = ApplicationService::create( + &"".to_string(), db, APPLICATION_ID, &plain_text_password, diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index 2d69d48..30321d7 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -52,7 +52,7 @@ mod tests { let db = get_memory_sqlite_connection().await; - let application = ApplicationService::create(&db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); + let application = ApplicationService::create(&"".to_string(), &db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); assert_eq!(application.id.to_owned(), 103151); assert_ne!(application.password.to_owned(), SECRET.to_string()); @@ -66,7 +66,7 @@ mod tests { async fn test_candidate_session_correct_password() { let db = &get_memory_sqlite_connection().await; - let application = ApplicationService::create(&db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); + let application = ApplicationService::create(&"".to_string(), &db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); // correct password let session = ApplicationService::new_session( @@ -89,7 +89,7 @@ mod tests { async fn test_candidate_session_incorrect_password() { let db = &get_memory_sqlite_connection().await; - let application = ApplicationService::create(&db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); + let application = ApplicationService::create(&"".to_string(), &db, 103151, &SECRET.to_string(), "".to_string()).await.unwrap(); // incorrect password assert!(ApplicationService::new_session( From 3d8487f7712b5f362e4d2ebcfcfda4d9715895d7 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 16:43:00 +0100 Subject: [PATCH 05/46] feat: link candidates, so both can decrypt their personal id number --- core/src/services/application_service.rs | 53 +++++++++++++++++------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 969c104..02a550f 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -43,19 +43,15 @@ impl ApplicationService { priv_key_plain_text, plain_text_password.to_string() ).await?; - - let recipients = get_recipients(db, &pubkey).await?; - let enc_personal_id_number = EncryptedString::new( - &personal_id_number, - &recipients, - ).await?; - - let candidate = Self::find_or_create_candidate_with_personal_id( + + + let (candidate, enc_personal_id_number) = Self::find_or_create_candidate_with_personal_id( admin_private_key, db, personal_id_number, - &enc_personal_id_number, + &pubkey, ).await?; + let application = Mutation::create_application( db, @@ -74,8 +70,9 @@ impl ApplicationService { admin_private_key: &String, db: &DbConn, personal_id_number: String, - enc_personal_id_number: &EncryptedString, - ) -> Result { + pubkey: &String, + // enc_personal_id_number: &EncryptedString, + ) -> Result<(candidate::Model, String), ServiceError> { let candidates = Query::list_candidates_full(db).await?; let ids_decrypted = futures::future::join_all( candidates.iter().map(|c| async {( @@ -92,16 +89,44 @@ impl ApplicationService { .iter() .filter(|(_, id)| id == &personal_id_number) .collect(); - // TODO: take the candidate id directly from the iterator + + let mut recipients = get_recipients(db, pubkey).await?; + if found_ids.iter().any(|(_, personal_id)| personal_id == &personal_id_number) { let candidate = Query::find_candidate_by_id(db, found_ids[0].0) .await? .ok_or(ServiceError::CandidateNotFound)?; + + let mut linked_applications_pubkeys = Query::find_applications_by_candidate_id(db, candidate.id) + .await? + .iter() + .map(|a| a.public_key.to_owned()) + .collect(); + + recipients.append(&mut linked_applications_pubkeys); + + + let enc_personal_id_number = EncryptedString::new( + &personal_id_number, + &recipients, + ).await?; + let candidate = Mutation::update_personal_id(db, candidate, &enc_personal_id_number.to_owned().to_string()).await?; println!("Candidates linked!"); - Ok(candidate) + Ok( + (candidate, enc_personal_id_number.to_string()) + ) } else { - CandidateService::create(db, enc_personal_id_number.to_owned().to_string()).await + let enc_personal_id_number = EncryptedString::new( + &personal_id_number, + &recipients, + ).await?; + Ok( + ( + CandidateService::create(db, enc_personal_id_number.to_owned().to_string()).await?, + enc_personal_id_number.to_string(), + ) + ) } } From fc176348d0515b6aa484e29e0dd007c14c691167 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 17:08:56 +0100 Subject: [PATCH 06/46] fix: list applications test --- core/src/services/candidate_service.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 17baa49..2641786 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -97,16 +97,16 @@ pub mod tests { const APPLICATION_ID: i32 = 103151; #[tokio::test] - async fn test_list_candidates() { + async fn test_list_applications() { let db = get_memory_sqlite_connection().await; let admin = create_admin(&db).await; let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); - let candidates = CandidateService::list_candidates(&private_key, &db, None, None).await.unwrap(); + let candidates = ApplicationService::list_applications(&private_key, &db).await.unwrap(); assert_eq!(candidates.len(), 0); put_user_data(&db).await; - let candidates = CandidateService::list_candidates(&private_key, &db, None, None).await.unwrap(); + let candidates = ApplicationService::list_applications(&private_key, &db).await.unwrap(); assert_eq!(candidates.len(), 1); } From 85c6a47232b0bd362b2a0d12faf5c83c080cddee Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 17:33:40 +0100 Subject: [PATCH 07/46] feat!: lock candidate details for application entities. who can't decrypt it --- api/src/routes/admin.rs | 7 +++---- api/src/routes/candidate.rs | 16 ++++++++++------ core/src/database/mutation/candidate.rs | 10 ++++++---- core/src/database/query/application.rs | 2 +- core/src/error.rs | 5 ++++- core/src/services/application_service.rs | 17 ++++++++++++----- core/src/services/candidate_service.rs | 12 +++++++++--- entity/src/candidate.rs | 1 + .../src/m20221024_121621_create_candidate.rs | 2 ++ 9 files changed, 48 insertions(+), 24 deletions(-) diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index b92a97f..f09934c 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -166,13 +166,12 @@ pub async fn get_candidate( .await .map_err(|e| to_custom_error(ServiceError::DbError(e)))? .ok_or(to_custom_error(ServiceError::CandidateNotFound))?; - let candidate = ApplicationService::find_related_candidate(db, application).await - .map_err(to_custom_error)?; let details = ApplicationService::decrypt_all_details( private_key, db, - candidate + &application, + false, ) .await .map_err(to_custom_error)?; @@ -194,7 +193,7 @@ pub async fn delete_candidate( .await .map_err(|e| to_custom_error(ServiceError::DbError(e)))? .ok_or(to_custom_error(ServiceError::CandidateNotFound))?; - let candidate = ApplicationService::find_related_candidate(db, application).await.map_err(to_custom_error)?; + let candidate = ApplicationService::find_related_candidate(db, &application).await.map_err(to_custom_error)?; // TODO diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index dbe69c2..832b704 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -77,7 +77,7 @@ pub async fn whoami(conn: Connection<'_, Db>, session: ApplicationAuth) -> Resul let private_key = session.get_private_key(); let application: entity::application::Model = session.into(); - let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO + let candidate = ApplicationService::find_related_candidate(&db, &application).await.map_err(to_custom_error)?; // TODO println!("candidate: {:?}", candidate); let response = NewCandidateResponse::from_encrypted(&private_key, candidate).await .map_err(to_custom_error)?; @@ -95,9 +95,9 @@ pub async fn post_details( let db = conn.into_inner(); let form = details.into_inner(); let application: application::Model = session.into(); - let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO + let candidate = ApplicationService::find_related_candidate(&db, &application).await.map_err(to_custom_error)?; // TODO - let _candidate_parent = ApplicationService::add_all_details(db, &application.public_key, candidate, &form) + let _candidate_parent = ApplicationService::add_all_details(db, &application, candidate, &form) .await .map_err(to_custom_error)?; @@ -112,9 +112,13 @@ pub async fn get_details( let db = conn.into_inner(); let private_key = session.get_private_key(); let application: entity::application::Model = session.into(); - let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO - let details = ApplicationService::decrypt_all_details(private_key, db, candidate) + let details = ApplicationService::decrypt_all_details( + private_key, + db, + &application, + true + ) .await .map(|x| Json(x)) .map_err(to_custom_error); @@ -218,7 +222,7 @@ pub async fn submit_portfolio( let db = conn.into_inner(); let application: entity::application::Model = session.into(); - let candidate = ApplicationService::find_related_candidate(&db, application.clone()).await.map_err(to_custom_error)?; // TODO + let candidate = ApplicationService::find_related_candidate(&db, &application).await.map_err(to_custom_error)?; // TODO let submit = PortfolioService::submit(&application.public_key, &candidate, &db).await; diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 2f964a8..1994b82 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -44,11 +44,12 @@ impl Mutation { pub async fn update_candidate_details( db: &DbConn, - user: candidate::Model, + candidate: candidate::Model, enc_candidate: EncryptedCandidateDetails, + encrypted_by_id: i32, ) -> Result { - let application = user.id; - let mut candidate: candidate::ActiveModel = user.into(); + let application = candidate.id; + let mut candidate: candidate::ActiveModel = candidate.into(); candidate.name = Set(enc_candidate.name.map(|e| e.into())); candidate.surname = Set(enc_candidate.surname.map(|e| e.into())); @@ -63,6 +64,7 @@ impl Mutation { candidate.school_name = Set(enc_candidate.school_name.map(|e| e.into())); candidate.health_insurance = Set(enc_candidate.health_insurance.map(|e| e.into())); candidate.study = Set(enc_candidate.study.map(|e| e.into())); + candidate.encrypted_by_id = Set(Some(encrypted_by_id)); candidate.updated_at = Set(chrono::offset::Local::now().naive_local()); @@ -128,7 +130,7 @@ mod tests { vec!["age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_string()], ).await.unwrap(); - let candidate = Mutation::update_candidate_details(&db, candidate, encrypted_details.candidate).await.unwrap(); + let candidate = Mutation::update_candidate_details(&db, candidate, encrypted_details.candidate, 1).await.unwrap(); let candidate = Query::find_candidate_by_id(&db, candidate.id) .await diff --git a/core/src/database/query/application.rs b/core/src/database/query/application.rs index 54974f7..2ec4077 100644 --- a/core/src/database/query/application.rs +++ b/core/src/database/query/application.rs @@ -27,7 +27,7 @@ impl Query { pub async fn find_related_candidate( db: &DbConn, - application: application::Model, + application: &application::Model, ) -> Result, DbErr> { application .find_related(candidate::Entity) diff --git a/core/src/error.rs b/core/src/error.rs index 72bbca0..adecdfb 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -18,6 +18,8 @@ pub enum ServiceError { UserAlreadyExists, #[error("Candidate not found")] CandidateNotFound, + #[error("Resource is locked")] + Locked, #[error("Parrent not found")] ParentNotFound, #[error("Database error")] @@ -69,7 +71,7 @@ pub enum ServiceError { impl ServiceError { pub fn code(&self) -> u16 { match self { - // 40X + // 4XX ServiceError::InvalidApplicationId => 400, ServiceError::ParentOverflow => 400, ServiceError::Unauthorized => 401, @@ -79,6 +81,7 @@ impl ServiceError { ServiceError::CandidateNotFound => 404, ServiceError::IncompletePortfolio => 406, ServiceError::UserAlreadyExists => 409, + ServiceError::Locked => 423, // 500 ServiceError::ParentNotFound => 500, ServiceError::DbError(_) => 500, diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 02a550f..c1a7570 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -160,7 +160,7 @@ impl ApplicationService { pub async fn find_related_candidate( db: &DbConn, - application: application::Model, + application: &application::Model, ) -> Result { let candidate = Query::find_related_candidate(db, application).await?; if let Some(candidate) = candidate { @@ -172,13 +172,13 @@ impl ApplicationService { pub async fn add_all_details( db: &DbConn, - public_key: &String, + application: &application::Model, candidate: candidate::Model, form: &ApplicationDetails, ) -> Result<(candidate::Model, Vec), ServiceError> { - let recipients = get_recipients(db, public_key).await?; - let candidate = CandidateService::add_candidate_details(db, candidate, &form.candidate, &recipients).await?; + let recipients = get_recipients(db, &application.public_key).await?; + let candidate = CandidateService::add_candidate_details(db, candidate, &form.candidate, &recipients, application.id).await?; let parents = ParentService::add_parents_details(db, &candidate, &form.parents, &recipients).await?; Ok( ( @@ -191,8 +191,15 @@ impl ApplicationService { pub async fn decrypt_all_details( private_key: String, db: &DbConn, - candidate: candidate::Model, + application: &application::Model, + restrict_access: bool, ) -> Result { + let candidate = ApplicationService::find_related_candidate(db, application).await?; + + if restrict_access && candidate.encrypted_by_id.is_some() && candidate.encrypted_by_id != Some(application.id) { + return Err(ServiceError::Locked) + } + let parents = Query::find_candidate_parents(db, &candidate).await?; let enc_details = EncryptedApplicationDetails::from((&candidate, parents)); diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 2641786..ab0608e 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -48,9 +48,15 @@ impl CandidateService { candidate: candidate::Model, details: &CandidateDetails, recipients: &Vec, + encrypted_by: i32, ) -> Result { let enc_details = EncryptedCandidateDetails::new(&details, recipients).await?; - let model = Mutation::update_candidate_details(db, candidate, enc_details).await?; + let model = Mutation::update_candidate_details( + db, + candidate, + enc_details, + encrypted_by + ).await?; Ok(model) } @@ -148,11 +154,11 @@ pub mod tests { "0000001111".to_string() ).await.unwrap(); - let candidate= ApplicationService::find_related_candidate(db, application.to_owned()).await.unwrap(); + let candidate= ApplicationService::find_related_candidate(db, &application).await.unwrap(); let form = APPLICATION_DETAILS.lock().unwrap().clone(); - let (candidate, parents) = ApplicationService::add_all_details(&db, &application.public_key, candidate, &form) + let (candidate, parents) = ApplicationService::add_all_details(&db, &application, candidate, &form) .await .unwrap(); diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index 79f96bc..c1a1d77 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -21,6 +21,7 @@ pub struct Model { pub personal_identification_number: String, pub school_name: Option, pub health_insurance: Option, + pub encrypted_by_id: Option, pub created_at: DateTime, pub updated_at: DateTime, } diff --git a/migration/src/m20221024_121621_create_candidate.rs b/migration/src/m20221024_121621_create_candidate.rs index c822cd4..0b4f77b 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -32,6 +32,7 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Candidate::PersonalIdentificationNumber).string().not_null()) .col(ColumnDef::new(Candidate::SchoolName).string()) .col(ColumnDef::new(Candidate::HealthInsurance).string()) + .col(ColumnDef::new(Candidate::EncryptedById).integer()) .col(ColumnDef::new(Candidate::CreatedAt).date_time().not_null()) .col(ColumnDef::new(Candidate::UpdatedAt).date_time().not_null()) .to_owned(), @@ -64,6 +65,7 @@ pub enum Candidate { PersonalIdentificationNumber, SchoolName, HealthInsurance, + EncryptedById, CreatedAt, UpdatedAt, } From 0595f3c044e0bf6994c3ea1e0ab29558dfde92ea Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 17:37:27 +0100 Subject: [PATCH 08/46] refactor: optimize imports --- core/src/database/mutation/application.rs | 2 +- core/src/database/mutation/candidate.rs | 9 --------- core/src/database/query/session.rs | 6 +++--- core/src/services/application_service.rs | 20 +------------------- core/src/services/candidate_service.rs | 17 +++++++---------- core/src/services/parent_service.rs | 4 +--- 6 files changed, 13 insertions(+), 45 deletions(-) diff --git a/core/src/database/mutation/application.rs b/core/src/database/mutation/application.rs index 1589c07..0187360 100644 --- a/core/src/database/mutation/application.rs +++ b/core/src/database/mutation/application.rs @@ -1,6 +1,6 @@ use ::entity::application; use log::{info, warn}; -use sea_orm::{DbConn, DbErr, Set, ActiveModelTrait, EntityTrait, IntoActiveModel, QueryFilter, ColumnTrait}; +use sea_orm::{DbConn, DbErr, Set, ActiveModelTrait, IntoActiveModel}; use crate::Mutation; diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 1994b82..bb9f802 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -22,15 +22,6 @@ impl Mutation { Ok(candidate) } - pub async fn find_related_application( - db: &DbConn, - candidate: candidate::Model, - ) -> Result, DbErr> { - candidate.find_related(application::Entity) - .one(db) - .await - } - pub async fn delete_candidate( db: &DbConn, candidate: candidate::Model, diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index 0e86509..f7db5cd 100644 --- a/core/src/database/query/session.rs +++ b/core/src/database/query/session.rs @@ -1,7 +1,7 @@ use crate::Query; use ::entity::prelude::AdminSession; -use ::entity::{candidate, admin, admin_session, application}; +use ::entity::{admin, admin_session, application}; use ::entity::{session, session::Entity as Session}; use sea_orm::prelude::Uuid; use sea_orm::*; @@ -38,12 +38,12 @@ impl Query { #[cfg(test)] mod tests { - use entity::{session, admin, candidate, admin_session, application}; + use entity::{session, admin, admin_session}; use sea_orm::{prelude::Uuid, ActiveModelTrait, Set}; use crate::services::candidate_service::tests::put_user_data; use crate::utils::db::get_memory_sqlite_connection; - use crate::{Query, Mutation}; + use crate::{Query}; #[tokio::test] async fn test_find_session_by_uuid() { diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index c1a7570..6002adb 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -5,7 +5,7 @@ use sea_orm::{DbConn, prelude::Uuid, IntoActiveModel}; use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::{candidate::ApplicationDetails, candidate_details::EncryptedString, auth::AuthenticableTrait, application::ApplicationResponse}, Mutation, crypto::{hash_password, self}}; -use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService, portfolio_service::PortfolioService}; +use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService}; const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; @@ -140,24 +140,6 @@ impl ApplicationService { FIELD_OF_STUDY_PREFIXES.contains(&field_of_study_prefix) } - pub async fn create_candidate_with_parent( // uchazeč s maminkou 👩‍🍼 - db: &DbConn, - application: application::Model, - plain_text_password: &String, - personal_id_number: String, - ) -> Result<(application::Model, candidate::Model, parent::Model), ServiceError> { - let candidate = CandidateService::create(db, personal_id_number).await?; - let parent = ParentService::create(db, candidate.id).await?; - let application = Mutation::update_candidate_fk(db, application, candidate.id).await?; - Ok( - ( - application, - candidate, - parent - ) - ) - } - pub async fn find_related_candidate( db: &DbConn, application: &application::Model, diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index ab0608e..7bc4828 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,16 +1,13 @@ -use async_trait::async_trait; -use chrono::Duration; -use entity::{candidate, session, application}; -use sea_orm::{prelude::Uuid, DbConn, IntoActiveModel}; +use entity::candidate; +use sea_orm::DbConn; use crate::{ - models::{candidate_details::{EncryptedApplicationDetails, EncryptedString, EncryptedCandidateDetails}, candidate::CandidateDetails}, - crypto::{self, hash_password}, + models::{candidate_details::EncryptedCandidateDetails, candidate::CandidateDetails}, error::ServiceError, - Mutation, Query, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse}, auth::AuthenticableTrait}, utils::db::get_recipients, + Mutation, Query, models::candidate::BaseCandidateResponse, }; -use super::{session_service::SessionService, portfolio_service::PortfolioService}; +use super::{portfolio_service::PortfolioService}; pub struct CandidateService; @@ -93,7 +90,7 @@ pub mod tests { use crate::models::candidate_details::tests::assert_all_application_details; use crate::utils::db::get_memory_sqlite_connection; - use crate::{crypto, services::candidate_service::CandidateService, Mutation}; + use crate::{crypto}; use crate::models::candidate_details::EncryptedApplicationDetails; use entity::{application, candidate, parent, admin}; @@ -172,7 +169,7 @@ pub mod tests { #[tokio::test] async fn test_put_user_data() { let db = get_memory_sqlite_connection().await; - let (application, candidate, parents) = put_user_data(&db).await; + let (_, candidate, parents) = put_user_data(&db).await; assert!(candidate.name.is_some()); assert!(parents[0].name.is_some()); } diff --git a/core/src/services/parent_service.rs b/core/src/services/parent_service.rs index 14655df..fd3cb2e 100644 --- a/core/src/services/parent_service.rs +++ b/core/src/services/parent_service.rs @@ -54,9 +54,7 @@ mod tests { use once_cell::sync::Lazy; - use crate::{utils::db::get_memory_sqlite_connection, models::{candidate::{ParentDetails, ApplicationDetails, CandidateDetails}, candidate_details::EncryptedApplicationDetails}, services::{candidate_service::CandidateService, application_service::ApplicationService}, crypto}; - - use super::ParentService; + use crate::{utils::db::get_memory_sqlite_connection, models::{candidate::{ParentDetails, ApplicationDetails, CandidateDetails}}, services::{candidate_service::CandidateService}}; pub static APPLICATION_DETAILS_TWO_PARENTS: Lazy> = Lazy::new(|| Mutex::new(ApplicationDetails { From 156bd3e7396bf39f94710069249d1762337c9e60 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 22:08:31 +0100 Subject: [PATCH 09/46] refactor: remove study field --- api/src/routes/candidate.rs | 3 +-- core/src/database/mutation/candidate.rs | 3 +-- core/src/database/query/application.rs | 2 -- core/src/database/query/candidate.rs | 24 ----------------- core/src/models/application.rs | 2 -- core/src/models/candidate.rs | 1 - core/src/models/candidate_details.rs | 10 +------ core/src/services/candidate_service.rs | 26 ------------------- core/src/services/parent_service.rs | 1 - core/src/utils/csv.rs | 2 +- entity/src/candidate.rs | 1 - .../src/m20221024_121621_create_candidate.rs | 1 - .../m20230114_114628_create_application.rs | 12 ++++++++- 13 files changed, 15 insertions(+), 73 deletions(-) diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index 832b704..ed3e0aa 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -310,8 +310,7 @@ mod tests { \"sex\": \"MALE\", \"personalIdNumber\": \"0101010000\", \"schoolName\": \"29988383\", - \"healthInsurance\": \"000\", - \"study\": \"KB\" + \"healthInsurance\": \"000\" }, \"parents\": [ { diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index bb9f802..8dac3f3 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -54,7 +54,6 @@ impl Mutation { candidate.personal_identification_number = Set(enc_candidate.personal_id_number.map(|e| e.into()).unwrap_or_default()); // TODO: do not set this here, it is already set in the create_candidate mutation??? candidate.school_name = Set(enc_candidate.school_name.map(|e| e.into())); candidate.health_insurance = Set(enc_candidate.health_insurance.map(|e| e.into())); - candidate.study = Set(enc_candidate.study.map(|e| e.into())); candidate.encrypted_by_id = Set(Some(encrypted_by_id)); candidate.updated_at = Set(chrono::offset::Local::now().naive_local()); @@ -127,6 +126,6 @@ mod tests { .await .unwrap().unwrap(); - assert!(candidate.study.is_some()); + assert!(candidate.name.is_some()); } } diff --git a/core/src/database/query/application.rs b/core/src/database/query/application.rs index 2ec4077..4b2baef 100644 --- a/core/src/database/query/application.rs +++ b/core/src/database/query/application.rs @@ -10,7 +10,6 @@ pub struct ApplicationCandidateJoin { pub surname: Option, pub email: Option, pub telephone: Option, - pub study: Option, } use crate::Query; @@ -47,7 +46,6 @@ impl Query { .column_as(candidate::Column::Surname, "surname") .column_as(candidate::Column::Email, "email") .column_as(candidate::Column::Telephone, "telephone") - .column_as(candidate::Column::Study, "study") .into_model::() .all(db) .await diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index 6f32d21..461a802 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -44,30 +44,6 @@ impl Query { .await } - pub async fn list_candidates_preview( - db: &DbConn, - field_of_study_opt: Option, - page: Option, - ) -> Result, DbErr> { - let select = Candidate::find(); - let query = if let Some(study) = field_of_study_opt { - select.filter(candidate::Column::Study.eq(study)) - } else { - select - } - .order_by(candidate::Column::Id, Order::Asc) - .into_model::(); - - if let Some(page) = page { - query - .paginate(db, PAGE_SIZE) - .fetch_page(page).await - } else { - query - .all(db).await - } - } - pub async fn list_candidates_full( db: &DbConn ) -> Result, DbErr> { diff --git a/core/src/models/application.rs b/core/src/models/application.rs index 3ca4da8..8597e34 100644 --- a/core/src/models/application.rs +++ b/core/src/models/application.rs @@ -14,7 +14,6 @@ pub struct ApplicationResponse { pub surname: String, pub email: String, pub telephone: String, - pub study: String, } impl ApplicationResponse { @@ -34,7 +33,6 @@ impl ApplicationResponse { surname: surname.unwrap_or_default(), email: email.unwrap_or_default(), telephone: telephone.unwrap_or_default(), - study: c.study.unwrap_or_default(), candidate_id: c.candidate_id, } ) diff --git a/core/src/models/candidate.rs b/core/src/models/candidate.rs index 9a72cc7..38b7cc7 100644 --- a/core/src/models/candidate.rs +++ b/core/src/models/candidate.rs @@ -50,7 +50,6 @@ pub struct CandidateDetails { pub citizenship: String, pub email: String, pub sex: String, - pub study: String, pub personal_id_number: String, pub school_name: String, pub health_insurance: String, diff --git a/core/src/models/candidate_details.rs b/core/src/models/candidate_details.rs index 4a55f89..2ac72c2 100644 --- a/core/src/models/candidate_details.rs +++ b/core/src/models/candidate_details.rs @@ -26,7 +26,6 @@ pub struct EncryptedCandidateDetails { pub personal_id_number: Option, pub school_name: Option, pub health_insurance: Option, - pub study: Option, } #[derive(Debug, Clone)] @@ -148,7 +147,6 @@ impl EncryptedCandidateDetails { personal_id_number: d.9, school_name: d.10, health_insurance: d.11, - study: Some(form.study.clone()), } ) } @@ -182,7 +180,6 @@ impl EncryptedCandidateDetails { personal_id_number: d.9.unwrap_or_default(), school_name: d.10.unwrap_or_default(), health_insurance: d.11.unwrap_or_default(), - study: self.study.clone().unwrap_or_default(), } ) } @@ -197,8 +194,7 @@ impl EncryptedCandidateDetails { self.citizenship.is_some() && self.email.is_some() && self.sex.is_some() && - self.personal_id_number.is_some() && - self.study.is_some() + self.personal_id_number.is_some() } } impl From<&candidate::Model> for EncryptedCandidateDetails { @@ -218,7 +214,6 @@ impl From<&candidate::Model> for EncryptedCandidateDetails { personal_id_number: Some(EncryptedString::from(candidate.personal_identification_number.to_owned())), school_name: EncryptedString::try_from(&candidate.school_name).ok(), health_insurance: EncryptedString::try_from(&candidate.health_insurance).ok(), - study: candidate.study.clone(), } } } @@ -356,7 +351,6 @@ impl TryFrom for EncryptedApplicationDetails { personal_id_number: EncryptedString::try_from(&cp.personal_identification_number).ok(), school_name: EncryptedString::try_from(&cp.school_name).ok(), health_insurance: EncryptedString::try_from(&cp.health_insurance).ok(), - study: cp.study.ok_or(ServiceError::CandidateDetailsNotSet).ok(), }, parents: vec![EncryptedParentDetails { name: EncryptedString::try_from(&cp.parent_name).ok(), @@ -410,7 +404,6 @@ pub mod tests { personal_id_number: "personal_id_number".to_string(), school_name: "school_name".to_string(), health_insurance: "health_insurance".to_string(), - study: "study".to_string(), }, parents: vec![ParentDetails { name: "parent_name".to_string(), @@ -431,7 +424,6 @@ pub mod tests { assert_eq!(details.candidate.citizenship, "citizenship"); assert_eq!(details.candidate.email, "email"); assert_eq!(details.candidate.sex, "sex"); - assert_eq!(details.candidate.study, "study"); assert_eq!(details.candidate.personal_id_number, "personal_id_number"); for parent in &details.parents { assert_eq!(parent.name, "parent_name"); diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 7bc4828..f622c08 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -56,32 +56,6 @@ impl CandidateService { ).await?; Ok(model) } - - pub async fn list_candidates( - private_key: &String, - db: &DbConn, - field_of_study: Option, - page: Option, - ) -> Result, ServiceError> { - - let candidates = Query::list_candidates_preview( - db, - field_of_study, - page - ).await?; - - futures::future::try_join_all( - candidates - .iter() - .map(|c| async move { - BaseCandidateResponse::from_encrypted( - private_key, - c.clone(), - PortfolioService::get_submission_progress(c.application).await.ok() - ).await - }) - ).await - } } #[cfg(test)] diff --git a/core/src/services/parent_service.rs b/core/src/services/parent_service.rs index fd3cb2e..fbce151 100644 --- a/core/src/services/parent_service.rs +++ b/core/src/services/parent_service.rs @@ -71,7 +71,6 @@ mod tests { personal_id_number: "personal_id_number".to_string(), school_name: "school_name".to_string(), health_insurance: "health_insurance".to_string(), - study: "study".to_string(), }, parents: vec![ParentDetails { name: "parent_name".to_string(), diff --git a/core/src/utils/csv.rs b/core/src/utils/csv.rs index 6c3a3b2..4a48a82 100644 --- a/core/src/utils/csv.rs +++ b/core/src/utils/csv.rs @@ -15,7 +15,7 @@ impl From<(i32, ApplicationDetails)> for Row { citizenship: Some(c.citizenship), email: Some(c.email), sex: Some(c.sex), - study: Some(c.study), + study: Some("TODO".to_string()), health_insurance: Some(c.health_insurance), school_name: Some(c.school_name), personal_identification_number: Some(c.personal_id_number), diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index c1a1d77..9f88ce3 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -17,7 +17,6 @@ pub struct Model { pub citizenship: Option, pub email: Option, pub sex: Option, - pub study: Option, pub personal_identification_number: String, pub school_name: Option, pub health_insurance: Option, diff --git a/migration/src/m20221024_121621_create_candidate.rs b/migration/src/m20221024_121621_create_candidate.rs index 0b4f77b..3a745d9 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -28,7 +28,6 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Candidate::Citizenship).string()) .col(ColumnDef::new(Candidate::Email).string()) .col(ColumnDef::new(Candidate::Sex).string()) - .col(ColumnDef::new(Candidate::Study).string()) .col(ColumnDef::new(Candidate::PersonalIdentificationNumber).string().not_null()) .col(ColumnDef::new(Candidate::SchoolName).string()) .col(ColumnDef::new(Candidate::HealthInsurance).string()) diff --git a/migration/src/m20230114_114628_create_application.rs b/migration/src/m20230114_114628_create_application.rs index 48330ab..004dbd3 100644 --- a/migration/src/m20230114_114628_create_application.rs +++ b/migration/src/m20230114_114628_create_application.rs @@ -26,7 +26,17 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Application::UpdatedAt).date_time().not_null()) .to_owned(), ) - .await + .await?; + + manager.create_index( + Index::create() + .name("idx_application_candidate_id") + .table(Application::Table) + .col(Application::CandidateId) + .to_owned(), + ).await?; + + Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { From eaddeaedd425972606097093168af2c3326969b0 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 22:18:03 +0100 Subject: [PATCH 10/46] feat: password reset for application --- api/src/routes/admin.rs | 18 ++++++---------- core/src/models/application.rs | 2 +- core/src/models/candidate_details.rs | 2 +- core/src/services/application_service.rs | 27 ++++++++++++++---------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index f09934c..f411ae4 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -125,9 +125,6 @@ pub async fn list_candidates( } - /* let candidates = CandidateService::list_candidates(&private_key, db, field, page) - .await - .map_err(to_custom_error)?; */ let candidates = ApplicationService::list_applications(&private_key, db) .await.map_err(to_custom_error)?; @@ -207,19 +204,18 @@ pub async fn reset_candidate_password( conn: Connection<'_, Db>, session: AdminAuth, id: i32, -) -> Result<(), Custom> { +) -> Result, Custom> { // TODO - /* let db = conn.into_inner(); + let db = conn.into_inner(); let private_key = session.get_private_key(); - let response = CandidateService::reset_password(private_key, db, id) + let response = ApplicationService::reset_password(private_key, db, id) .await - .map_err(to_custom_error)?; */ - - Ok(()) - /* Ok( + .map_err(to_custom_error)?; + + Ok( Json(response) - ) */ + ) } #[get("/candidate//portfolio")] diff --git a/core/src/models/application.rs b/core/src/models/application.rs index 8597e34..19f9fd6 100644 --- a/core/src/models/application.rs +++ b/core/src/models/application.rs @@ -1,6 +1,6 @@ use serde::{Serialize, Deserialize}; -use crate::{database::query::application::ApplicationCandidateJoin, services::portfolio_service::SubmissionProgress, error::ServiceError}; +use crate::{database::query::application::ApplicationCandidateJoin, error::ServiceError}; use super::candidate_details::EncryptedString; diff --git a/core/src/models/candidate_details.rs b/core/src/models/candidate_details.rs index 2ac72c2..315453a 100644 --- a/core/src/models/candidate_details.rs +++ b/core/src/models/candidate_details.rs @@ -502,7 +502,7 @@ pub mod tests { let db = get_memory_sqlite_connection().await; let _admin = insert_test_admin(&db).await; - let (application, candidate, parents) = put_user_data(&db).await; + let (_, candidate, parents) = put_user_data(&db).await; let encrypted_details = EncryptedApplicationDetails::try_from((&candidate, parents)).unwrap(); diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 6002adb..3d7bb55 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -3,7 +3,7 @@ use chrono::Duration; use entity::{candidate, parent, application, session}; use sea_orm::{DbConn, prelude::Uuid, IntoActiveModel}; -use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::{candidate::ApplicationDetails, candidate_details::EncryptedString, auth::AuthenticableTrait, application::ApplicationResponse}, Mutation, crypto::{hash_password, self}}; +use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::{candidate::{ApplicationDetails, CreateCandidateResponse}, candidate_details::EncryptedString, auth::AuthenticableTrait, application::ApplicationResponse}, Mutation, crypto::{hash_password, self}}; use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService}; @@ -235,16 +235,16 @@ impl ApplicationService { } // TODO - /* pub async fn reset_password( + pub async fn reset_password( admin_private_key: String, db: &DbConn, id: i32, ) -> Result { - let candidate = Query::find_candidate_by_id(db, id).await? + let application = Query::find_application_by_id(db, id).await? .ok_or(ServiceError::CandidateNotFound)?; + let candidate = ApplicationService::find_related_candidate(db, &application).await?; let parents = Query::find_candidate_parents(db, &candidate).await?; - - + let new_password_plain = crypto::random_12_char_string(); let new_password_hash = crypto::hash_password(new_password_plain.clone()).await?; @@ -254,9 +254,9 @@ impl ApplicationService { ).await?; - Self::delete_old_sessions(db, &candidate, 0).await?; - let candidate = Mutation::update_candidate_password_and_keys(db, - candidate, + Self::delete_old_sessions(db, &application, 0).await?; + let application = Mutation::update_application_password_and_keys(db, + application, new_password_hash, pubkey.clone(), encrypted_priv_key @@ -264,7 +264,7 @@ impl ApplicationService { // user might no have filled his details yet, but personal id number is filled from beginning - let personal_id_number = EncryptedString::from(candidate.personal_identification_number.clone()) + let personal_id_number = EncryptedString::from(application.personal_id_number.clone()) .decrypt(&admin_private_key) .await?; @@ -274,7 +274,12 @@ impl ApplicationService { .decrypt(admin_private_key).await?; let enc_details = EncryptedApplicationDetails::new(&dec_details, recipients).await?; - Mutation::update_candidate_details(db, candidate, enc_details.candidate).await?; + Mutation::update_candidate_details(db, + candidate, + enc_details.candidate, + application.id + ).await?; + for i in 0..enc_details.parents.len() { Mutation::add_parent_details(db, parents[i].clone(), enc_details.parents[i].clone()).await?; } @@ -286,7 +291,7 @@ impl ApplicationService { password: new_password_plain, } ) - } */ + } } #[async_trait] From b0dd796dfea80794fe486ca346e03005fa0e730e Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 22:37:07 +0100 Subject: [PATCH 11/46] feat: limit application count to 2 per candidate --- core/src/error.rs | 3 +++ core/src/services/application_service.rs | 6 +++++- core/src/services/portfolio_service.rs | 22 ++++++++-------------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/core/src/error.rs b/core/src/error.rs index adecdfb..40303ac 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -20,6 +20,8 @@ pub enum ServiceError { CandidateNotFound, #[error("Resource is locked")] Locked, + #[error("Too many applications")] + TooManyApplications, #[error("Parrent not found")] ParentNotFound, #[error("Database error")] @@ -82,6 +84,7 @@ impl ServiceError { ServiceError::IncompletePortfolio => 406, ServiceError::UserAlreadyExists => 409, ServiceError::Locked => 423, + ServiceError::TooManyApplications => 409, // 500 ServiceError::ParentNotFound => 500, ServiceError::DbError(_) => 500, diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 3d7bb55..f9f9d1b 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -97,12 +97,16 @@ impl ApplicationService { .await? .ok_or(ServiceError::CandidateNotFound)?; - let mut linked_applications_pubkeys = Query::find_applications_by_candidate_id(db, candidate.id) + let mut linked_applications_pubkeys: Vec = Query::find_applications_by_candidate_id(db, candidate.id) .await? .iter() .map(|a| a.public_key.to_owned()) .collect(); + if linked_applications_pubkeys.len() > 1 { + return Err(ServiceError::TooManyApplications); + } + recipients.append(&mut linked_applications_pubkeys); diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index ec0dc05..e03ffcf 100644 --- a/core/src/services/portfolio_service.rs +++ b/core/src/services/portfolio_service.rs @@ -8,7 +8,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{error::ServiceError, Query, crypto}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum SubmissionProgress { NoneInCache, SomeInCache(Vec), @@ -50,7 +50,7 @@ impl Serialize for SubmissionProgress { } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, PartialEq, Clone)] pub enum FileType { CoverLetterPdf = 1, PortfolioLetterPdf = 2, @@ -216,17 +216,7 @@ impl PortfolioService { /// Returns true if portfolio is ready to be moved to the final directory async fn is_portfolio_prepared(candidate_id: i32) -> bool { - let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); - - let filenames = vec![FileType::CoverLetterPdf, FileType::PortfolioLetterPdf, FileType::PortfolioZip]; - for filename in filenames { - if !tokio::fs::metadata( - cache_path.join(filename.as_str()) - ).await.is_ok() { - return false; - } - } - true + Self::get_submission_progress(candidate_id).await.ok() == Some(SubmissionProgress::AllInCache) } // Delete single item from cache @@ -306,10 +296,14 @@ impl PortfolioService { writer.close().await?; archive.shutdown().await?; + let applications_pubkeys: Vec = Query::find_applications_by_candidate_id(db, candidate_id) + .await?.iter().map(|a| a.public_key.to_owned()).collect(); let admin_public_keys = Query::get_all_admin_public_keys(db).await?; let mut admin_public_keys_refrence: Vec<&str> = admin_public_keys.iter().map(|s| &**s).collect(); - let mut recipients = vec![&**public_key]; + + let mut recipients = vec![]; recipients.append(&mut admin_public_keys_refrence); + recipients.append(&mut applications_pubkeys.iter().map(|s| &**s).collect()); let final_path = path.join(FileType::PortfolioZip.as_str()); From bcb43b622ce9b9749d2161f3b8a5bc778176538d Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 22:58:37 +0100 Subject: [PATCH 12/46] feat: all application_ids on whoami --- api/src/routes/candidate.rs | 9 ++++++--- core/src/models/candidate.rs | 9 +++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index ed3e0aa..6b77bc6 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -2,6 +2,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use entity::application; use portfolio_core::Query; +use portfolio_core::error::ServiceError; use portfolio_core::models::auth::AuthenticableTrait; use portfolio_core::models::candidate::{ApplicationDetails, NewCandidateResponse}; use portfolio_core::sea_orm::prelude::Uuid; @@ -77,9 +78,11 @@ pub async fn whoami(conn: Connection<'_, Db>, session: ApplicationAuth) -> Resul let private_key = session.get_private_key(); let application: entity::application::Model = session.into(); - let candidate = ApplicationService::find_related_candidate(&db, &application).await.map_err(to_custom_error)?; // TODO - println!("candidate: {:?}", candidate); - let response = NewCandidateResponse::from_encrypted(&private_key, candidate).await + let candidate = ApplicationService::find_related_candidate(&db, &application) + .await.map_err(to_custom_error)?; // TODO more compact + let applications = Query::find_applications_by_candidate_id(&db, candidate.id) + .await.map_err(|e| to_custom_error(ServiceError::DbError(e)))?; + let response = NewCandidateResponse::from_encrypted(applications, &private_key, candidate).await .map_err(to_custom_error)?; Ok(Json(response)) diff --git a/core/src/models/candidate.rs b/core/src/models/candidate.rs index 38b7cc7..9f536d6 100644 --- a/core/src/models/candidate.rs +++ b/core/src/models/candidate.rs @@ -1,5 +1,5 @@ use chrono::NaiveDate; -use entity::candidate; +use entity::{candidate, application}; use sea_orm::FromQueryResult; use serde::{Serialize, Deserialize}; @@ -11,7 +11,7 @@ use super::candidate_details::EncryptedString; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NewCandidateResponse { - pub application_id: i32, + pub applications: Vec, pub personal_id_number: String, } @@ -103,11 +103,12 @@ pub struct Row { } impl NewCandidateResponse { - pub async fn from_encrypted(private_key: &String, c: candidate::Model) -> Result { + pub async fn from_encrypted(applications: Vec, private_key: &String, c: candidate::Model) -> Result { let id_number = EncryptedString::from(c.personal_identification_number).decrypt(private_key).await?; + let applications = applications.iter().map(|a| a.id).collect(); Ok( Self { - application_id: c.id, + applications, personal_id_number: id_number, } ) From 2e68b33eefbc5b4b45774f793da909b55e38ea12 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 23:16:50 +0100 Subject: [PATCH 13/46] feat: validate field of study so one person can't apply to two same fields --- core/src/services/application_service.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index f9f9d1b..5c4d06c 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -46,6 +46,7 @@ impl ApplicationService { let (candidate, enc_personal_id_number) = Self::find_or_create_candidate_with_personal_id( + application_id, admin_private_key, db, personal_id_number, @@ -67,6 +68,7 @@ impl ApplicationService { } async fn find_or_create_candidate_with_personal_id( + application_id: i32, admin_private_key: &String, db: &DbConn, personal_id_number: String, @@ -100,9 +102,13 @@ impl ApplicationService { let mut linked_applications_pubkeys: Vec = Query::find_applications_by_candidate_id(db, candidate.id) .await? .iter() + .filter(|a| a.id.to_string()[0..3] != application_id.to_string()[0..3]) .map(|a| a.public_key.to_owned()) .collect(); + if linked_applications_pubkeys.is_empty() { + return Err(ServiceError::InvalidApplicationId); + } if linked_applications_pubkeys.len() > 1 { return Err(ServiceError::TooManyApplications); } From d666a9f6f3a3966771925bac0348788685ba2ea9 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sun, 15 Jan 2023 11:52:06 +0100 Subject: [PATCH 14/46] feat: more data in BaseCandidate response --- api/src/routes/candidate.rs | 7 ++- core/src/models/candidate.rs | 82 ++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index 6b77bc6..a5e4ad2 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -82,7 +82,12 @@ pub async fn whoami(conn: Connection<'_, Db>, session: ApplicationAuth) -> Resul .await.map_err(to_custom_error)?; // TODO more compact let applications = Query::find_applications_by_candidate_id(&db, candidate.id) .await.map_err(|e| to_custom_error(ServiceError::DbError(e)))?; - let response = NewCandidateResponse::from_encrypted(applications, &private_key, candidate).await + let response = NewCandidateResponse::from_encrypted( + application.id, + applications, + &private_key, + candidate + ).await .map_err(to_custom_error)?; Ok(Json(response)) diff --git a/core/src/models/candidate.rs b/core/src/models/candidate.rs index 9f536d6..095c44e 100644 --- a/core/src/models/candidate.rs +++ b/core/src/models/candidate.rs @@ -1,18 +1,24 @@ use chrono::NaiveDate; -use entity::{candidate, application}; +use entity::{application, candidate}; use sea_orm::FromQueryResult; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -use crate::{error::ServiceError, database::query::candidate::CandidateResult, services::portfolio_service::SubmissionProgress}; +use crate::{ + database::query::candidate::CandidateResult, error::ServiceError, + services::portfolio_service::SubmissionProgress, +}; -use super::candidate_details::EncryptedString; +use super::candidate_details::{EncryptedString, EncryptedCandidateDetails}; /// Minimal candidate response containing database only not null fields #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NewCandidateResponse { + pub current_application: i32, pub applications: Vec, pub personal_id_number: String, + pub details_filled: bool, + pub encrypted_by: Option, } /// Create candidate (admin endpoint) @@ -103,15 +109,25 @@ pub struct Row { } impl NewCandidateResponse { - pub async fn from_encrypted(applications: Vec, private_key: &String, c: candidate::Model) -> Result { - let id_number = EncryptedString::from(c.personal_identification_number).decrypt(private_key).await?; + pub async fn from_encrypted( + current_application: i32, + applications: Vec, + private_key: &String, + c: candidate::Model, + ) -> Result { + let id_number = EncryptedString::from(c.personal_identification_number.to_owned()) + .decrypt(private_key) + .await?; let applications = applications.iter().map(|a| a.id).collect(); - Ok( - Self { - applications, - personal_id_number: id_number, - } - ) + let encrypted_details = EncryptedCandidateDetails::from(&c); + + Ok(Self { + current_application, + applications, + personal_id_number: id_number, + details_filled: encrypted_details.is_filled(), + encrypted_by: c.encrypted_by_id, + }) } } @@ -121,21 +137,31 @@ impl BaseCandidateResponse { c: CandidateResult, progress: Option, ) -> Result { - let name = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.name).ok(), private_key).await?; - let surname = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.surname).ok(), private_key).await?; - let email = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.email).ok(), private_key).await?; - let telephone = EncryptedString::decrypt_option(&EncryptedString::try_from(&c.telephone).ok(), private_key).await?; - - Ok( - Self { - application_id: c.application, - name: name.unwrap_or_default(), - surname: surname.unwrap_or_default(), - email: email.unwrap_or_default(), - telephone: telephone.unwrap_or_default(), - study: c.study.unwrap_or_default(), - progress: progress.unwrap_or(SubmissionProgress::NoneInCache), - } + let name = + EncryptedString::decrypt_option(&EncryptedString::try_from(&c.name).ok(), private_key) + .await?; + let surname = EncryptedString::decrypt_option( + &EncryptedString::try_from(&c.surname).ok(), + private_key, ) + .await?; + let email = + EncryptedString::decrypt_option(&EncryptedString::try_from(&c.email).ok(), private_key) + .await?; + let telephone = EncryptedString::decrypt_option( + &EncryptedString::try_from(&c.telephone).ok(), + private_key, + ) + .await?; + + Ok(Self { + application_id: c.application, + name: name.unwrap_or_default(), + surname: surname.unwrap_or_default(), + email: email.unwrap_or_default(), + telephone: telephone.unwrap_or_default(), + study: c.study.unwrap_or_default(), + progress: progress.unwrap_or(SubmissionProgress::NoneInCache), + }) } -} \ No newline at end of file +} From 5740f19724b4941466ccbc1d8a4f0cd7a2cc5d4e Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sun, 15 Jan 2023 12:55:27 +0100 Subject: [PATCH 15/46] feat: encrypt personal data with pubkeys of all applications --- core/src/services/application_service.rs | 14 ++++++++++---- core/src/services/candidate_service.rs | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 5c4d06c..936cb9c 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -5,7 +5,7 @@ use sea_orm::{DbConn, prelude::Uuid, IntoActiveModel}; use crate::{error::ServiceError, Query, utils::db::get_recipients, models::candidate_details::{EncryptedApplicationDetails}, models::{candidate::{ApplicationDetails, CreateCandidateResponse}, candidate_details::EncryptedString, auth::AuthenticableTrait, application::ApplicationResponse}, Mutation, crypto::{hash_password, self}}; -use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService}; +use super::{parent_service::ParentService, candidate_service::CandidateService, session_service::SessionService, portfolio_service::PortfolioService}; const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; @@ -63,6 +63,8 @@ impl ApplicationService { pubkey, encrypted_priv_key, ).await?; + + PortfolioService::create_user_dir(application.id).await?; Ok(application) } @@ -169,7 +171,11 @@ impl ApplicationService { form: &ApplicationDetails, ) -> Result<(candidate::Model, Vec), ServiceError> { - let recipients = get_recipients(db, &application.public_key).await?; + let mut recipients = get_recipients(db, &application.public_key).await?; + let applications = Query::find_applications_by_candidate_id(db, candidate.id).await?; + recipients.append(&mut applications.iter().map(|a| a.public_key.to_owned()).collect()); + + let candidate = CandidateService::add_candidate_details(db, candidate, &form.candidate, &recipients, application.id).await?; let parents = ParentService::add_parents_details(db, &candidate, &form.parents, &recipients).await?; Ok( @@ -188,9 +194,9 @@ impl ApplicationService { ) -> Result { let candidate = ApplicationService::find_related_candidate(db, application).await?; - if restrict_access && candidate.encrypted_by_id.is_some() && candidate.encrypted_by_id != Some(application.id) { + /* if restrict_access && candidate.encrypted_by_id.is_some() && candidate.encrypted_by_id != Some(application.id) { return Err(ServiceError::Locked) - } + } */ let parents = Query::find_candidate_parents(db, &candidate).await?; let enc_details = EncryptedApplicationDetails::from((&candidate, parents)); diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index f622c08..55c0b03 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -4,7 +4,7 @@ use sea_orm::DbConn; use crate::{ models::{candidate_details::EncryptedCandidateDetails, candidate::CandidateDetails}, error::ServiceError, - Mutation, Query, models::candidate::BaseCandidateResponse, + Mutation, }; use super::{portfolio_service::PortfolioService}; @@ -27,7 +27,7 @@ impl CandidateService { ) .await?; - PortfolioService::create_user_dir(candidate.id).await?; + // PortfolioService::create_user_dir(candidate.id).await?; Ok(candidate) From cb25e713e6eb22d9a78123a0191f5db8d4f19575 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sun, 15 Jan 2023 13:03:43 +0100 Subject: [PATCH 16/46] fix: portfolio directory create path --- core/src/services/application_service.rs | 2 +- core/src/services/candidate_service.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 936cb9c..e268d8e 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -64,7 +64,7 @@ impl ApplicationService { encrypted_priv_key, ).await?; - PortfolioService::create_user_dir(application.id).await?; + // PortfolioService::create_user_dir(application.id).await?; Ok(application) } diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 55c0b03..5f7c63d 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -27,7 +27,7 @@ impl CandidateService { ) .await?; - // PortfolioService::create_user_dir(candidate.id).await?; + PortfolioService::create_user_dir(candidate.id).await?; Ok(candidate) From 6655e9978f4c9ab73b76088c7a9d5b0473f774a3 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sat, 14 Jan 2023 22:59:18 +0100 Subject: [PATCH 17/46] feat: two applications support candidate dashboard --- frontend/src/lib/@api/candidate.ts | 4 ++-- .../dashboard/DashboardInfoCard.svelte | 20 +++++++++++++++++-- frontend/src/lib/stores/candidate.ts | 9 +++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/@api/candidate.ts b/frontend/src/lib/@api/candidate.ts index 212b8c9..2b6f986 100644 --- a/frontend/src/lib/@api/candidate.ts +++ b/frontend/src/lib/@api/candidate.ts @@ -1,5 +1,5 @@ import axios, { type AxiosProgressEvent } from 'axios'; -import type { CandidateData, CandidateLogin, CreateCandidate } from '$lib/stores/candidate'; +import type { BaseCandidate, CandidateData, CandidateLogin, CreateCandidate } from '$lib/stores/candidate'; import type { SubmissionProgress } from '$lib/stores/portfolio'; import { API_URL, errorHandler, type Fetch } from '.'; import DOMPurify from 'isomorphic-dompurify'; @@ -51,7 +51,7 @@ export const apiFetchSubmissionProgress = async (fetchSsr?: Fetch): Promise => { +export const apiWhoami = async (fetchSsr?: Fetch): Promise => { const apiFetch = fetchSsr || fetch; try { console.log(API_URL + '/candidate/whoami'); diff --git a/frontend/src/lib/components/dashboard/DashboardInfoCard.svelte b/frontend/src/lib/components/dashboard/DashboardInfoCard.svelte index eab48b8..76ce604 100644 --- a/frontend/src/lib/components/dashboard/DashboardInfoCard.svelte +++ b/frontend/src/lib/components/dashboard/DashboardInfoCard.svelte @@ -65,6 +65,16 @@ } }; + const getField = (id: number) => { + if (id.toString().startsWith("101")) { + return 'G'; + } else if (id.toString().startsWith("102")) { + return 'IT'; + } else { + return 'KB'; + } + }; + const editDetails = async () => { goto('/register?edit=true'); }; @@ -140,9 +150,15 @@ class="mt-4 flex flex-col justify-between leading-10" > Ev. č. přihlášky: {$baseCandidateData.applicationId}Ev. č. přihlášky ({getField($baseCandidateData.applications[0])}): + {$baseCandidateData.applications[0]} + {#if $baseCandidateData.applications.length > 1} + Ev. č. přihlášky ({getField($baseCandidateData.applications[1])}): + {$baseCandidateData.applications[1]} + {/if} Obor: {$candidateData.candidate.study} Adresa: {$candidateData.candidate.address} ; + personalIdNumber: string; +} + export interface CreateCandidateLogin extends CreateCandidate { password: string; } -export const baseCandidateData = writable({ - applicationId: 0, +export const baseCandidateData = writable({ + applications: [], personalIdNumber: '' }); From d789c5b42d1f720bf95d68ff7d0a6e97426da663 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sun, 15 Jan 2023 11:51:45 +0100 Subject: [PATCH 18/46] feat: link account notification --- .../checkbox/AccountLinkCheckBox.svelte | 81 ++++++++++++++++++ frontend/src/lib/stores/candidate.ts | 7 +- .../(authenticated)/register/+page.svelte | 83 +++++++++++++++---- 3 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte diff --git a/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte b/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte new file mode 100644 index 0000000..465e731 --- /dev/null +++ b/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte @@ -0,0 +1,81 @@ + + +
+ switchSelection(0)} + class:error + on:change + type="checkbox" + id="linkOk" + checked={linkOk} + class="peer hidden" + /> + +
+
+ switchSelection(1)} + on:change + type="checkbox" + id="linkError" + checked={linkError} + class="peer hidden" + /> + +
+ + diff --git a/frontend/src/lib/stores/candidate.ts b/frontend/src/lib/stores/candidate.ts index 3199fe6..2f2e1b9 100644 --- a/frontend/src/lib/stores/candidate.ts +++ b/frontend/src/lib/stores/candidate.ts @@ -42,8 +42,11 @@ export interface CreateCandidate { } export interface BaseCandidate { + currentApplication: number; applications: Array; personalIdNumber: string; + detailsFilled: boolean; + encryptedBy?: number; } export interface CreateCandidateLogin extends CreateCandidate { @@ -51,8 +54,10 @@ export interface CreateCandidateLogin extends CreateCandidate { } export const baseCandidateData = writable({ + currentApplication: 0, applications: [], - personalIdNumber: '' + personalIdNumber: '', + detailsFilled: false }); export const candidateData = writable({ diff --git a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte index 2004550..2661533 100644 --- a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte +++ b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte @@ -20,10 +20,11 @@ import type { Writable } from 'svelte/store'; import * as yup from 'yup'; import type { CandidateData } from '$lib/stores/candidate'; + import AccountLinkCheckBox from '$lib/components/checkbox/AccountLinkCheckBox.svelte'; - const pageCount = 5; + const pageCount = 6; let pageIndex = 0; - let pagesFilled = [false, false, false, false, false]; + let pagesFilled = [false, false, false, false, false, false]; let pageTexts = [ 'Zpracování osobních údajů', 'Registrace', @@ -35,9 +36,15 @@ export let data: PageData; let details = data.candidate; + let baseCandidateDetails = data.whoami; + + let detailsFilledByAnotherAccount = baseCandidateDetails.encryptedBy !== null && + baseCandidateDetails.currentApplication !== baseCandidateDetails.encryptedBy; const formInitialValues = { gdpr: false, + linkOk: false, + linkError: false, candidate: { name: '', surname: '', @@ -75,6 +82,8 @@ const formValidationSchema = yup.object().shape({ gdpr: yup.boolean().oneOf([true]), + linkOk: yup.boolean().oneOf([true]), + linkError: yup.boolean().oneOf([false]), candidate: yup.object().shape({ name: yup.string().required(), surname: yup.string().required(), @@ -265,11 +274,16 @@ const isPageInvalid = (index: number): boolean => { switch (index) { case 0: - if ($typedErrors['gdpr']) { + if ($typedErrors['linkOk'] || $typedErrors['linkError']) { return true; } break; case 1: + if ($typedErrors['gdpr']) { + return true; + } + break; + case 2: if ( $typedErrors['candidate']['name'] || $typedErrors['candidate']['surname'] || @@ -280,7 +294,7 @@ } break; - case 2: + case 3: if ( $typedErrors['candidate']['birthplace'] || $typedErrors['candidate']['birthdate'] || @@ -293,7 +307,7 @@ return true; } break; - case 3: + case 4: if ( $typedErrors['parents'][0]['name'] || $typedErrors['parents'][0]['surname'] || @@ -303,7 +317,7 @@ return true; } break; - case 4: + case 5: if ( $typedErrors['parents'][1]['name'] || $typedErrors['parents'][1]['surname'] || @@ -313,7 +327,7 @@ return true; } break; - case 5: + case 6: if ( $typedErrors['candidate']['citizenship'] || $typedErrors['candidate']['personalIdNumber'] || @@ -343,6 +357,8 @@ ); form.set({ gdpr: true, + linkOk: true, + linkError: false, candidate: { ...details.candidate, street: details.candidate.address.split(',')[0].split(' ')[0], @@ -364,8 +380,8 @@ } ] }); - pageIndex = 1; // skip gdpr page - pageTexts[1] = 'Úprava osobních údajů'; + pageIndex = 2; // skip gdpr page + pageTexts[2] = 'Úprava osobních údajů'; } @@ -378,6 +394,45 @@ + {:else} +
+

Údaje již vyplněny

+

+ Vaše osobní údaje již byly vyplněny přes Váš druhý účet ({baseCandidateDetails.encryptedBy}). + Vaše údaje byly zaznamenány a Vaše přihlášky byly propojeny. +

+
+ +
+
+ {/if} + {:else if pageIndex === 1}

{pageTexts[0]}

@@ -394,7 +449,7 @@ />

- {:else if pageIndex === 1} + {:else if pageIndex === 2}

{pageTexts[1]}

@@ -429,7 +484,7 @@

- {:else if pageIndex === 2} + {:else if pageIndex === 3}

{pageTexts[2]}

Pro registraci je potřeba vyplnit několik údajů o Vás. Tyto údaje budou použity pro @@ -505,7 +560,7 @@ /> - {:else if pageIndex === 3} + {:else if pageIndex === 4}

{pageTexts[3]}

Sběr dat o zákonném zástupci je klíčový pro získání důležitých kontaktů a informací. @@ -537,7 +592,7 @@ /> - {:else if pageIndex === 4} + {:else if pageIndex === 5}

{pageTexts[4]}

Zde můžete zadat údaje o druhém zákonném zástupci. Škole tím umožníte lépe komunikovat. @@ -569,7 +624,7 @@ /> - {:else if pageIndex === 5} + {:else if pageIndex === 6}

{pageTexts[5]}

Zadejte prosím své občanství, rodné číslo, či jeho alternativu Vaší země a obor na který From 3a631c435abd8a1286af455219babd23c4ddf71f Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sun, 15 Jan 2023 13:46:07 +0100 Subject: [PATCH 19/46] refactor: do not require sex to be filled --- core/src/models/candidate_details.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/models/candidate_details.rs b/core/src/models/candidate_details.rs index 315453a..018d1bb 100644 --- a/core/src/models/candidate_details.rs +++ b/core/src/models/candidate_details.rs @@ -193,7 +193,7 @@ impl EncryptedCandidateDetails { self.telephone.is_some() && self.citizenship.is_some() && self.email.is_some() && - self.sex.is_some() && + // self.sex.is_some() && self.personal_id_number.is_some() } } From 7be707ced81bc3a440a8f85fd67998ddc3645fbd Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sun, 15 Jan 2023 13:46:21 +0100 Subject: [PATCH 20/46] refactor: do not overwrite personal id number --- core/src/database/mutation/candidate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index 8dac3f3..0e61f3a 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -51,7 +51,7 @@ impl Mutation { candidate.citizenship = Set(enc_candidate.citizenship.map(|e| e.into())); candidate.email = Set(enc_candidate.email.map(|e| e.into())); candidate.sex = Set(enc_candidate.sex.map(|e| e.into())); - candidate.personal_identification_number = Set(enc_candidate.personal_id_number.map(|e| e.into()).unwrap_or_default()); // TODO: do not set this here, it is already set in the create_candidate mutation??? + // candidate.personal_identification_number = Set(enc_candidate.personal_id_number.map(|e| e.into()).unwrap_or_default()); // TODO: do not set this here, it is already set in the create_candidate mutation??? candidate.school_name = Set(enc_candidate.school_name.map(|e| e.into())); candidate.health_insurance = Set(enc_candidate.health_insurance.map(|e| e.into())); candidate.encrypted_by_id = Set(Some(encrypted_by_id)); From d94f8c6732831d2c7ca041931336aef02d66cc25 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Sun, 15 Jan 2023 13:47:20 +0100 Subject: [PATCH 21/46] feat: inform user of other data recipients --- .../checkbox/AccountLinkCheckBox.svelte | 22 +++++++++------ .../(authenticated)/register/+page.svelte | 28 ++----------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte b/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte index 465e731..6a96917 100644 --- a/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte +++ b/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte @@ -1,11 +1,17 @@ +