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 diff --git a/Cargo.lock b/Cargo.lock index c62b392..79477a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2155,14 +2155,14 @@ dependencies = [ [[package]] name = "portfolio" -version = "1.0.0" +version = "2.0.0" dependencies = [ "portfolio-api", ] [[package]] name = "portfolio-api" -version = "1.0.0" +version = "2.0.0" dependencies = [ "async-trait", "chrono", @@ -2193,7 +2193,7 @@ dependencies = [ [[package]] name = "portfolio-core" -version = "1.0.0" +version = "2.0.0" dependencies = [ "aes-gcm-siv", "age", @@ -2223,7 +2223,7 @@ dependencies = [ [[package]] name = "portfolio-entity" -version = "1.0.0" +version = "2.0.0" dependencies = [ "async-trait", "chrono", @@ -2232,7 +2232,7 @@ dependencies = [ [[package]] name = "portfolio-migration" -version = "1.0.0" +version = "2.0.0" dependencies = [ "chrono", "portfolio-entity", diff --git a/Cargo.toml b/Cargo.toml index 8323f87..8a65506 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "portfolio" -version = "1.0.0" +version = "2.0.0" authors = ["Vojtěch Jungmann", "Sebastian Pravda"] edition = "2021" publish = false diff --git a/api/Cargo.toml b/api/Cargo.toml index cbd201b..e37aa74 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "portfolio-api" -version = "1.0.0" +version = "2.0.0" edition = "2021" publish = false 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..8794a20 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::{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}; @@ -85,20 +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(); - ApplicationService::create_candidate_with_parent( - db, - form.application_id, - &plain_text_password, - form.personal_id_number.clone(), - ) + ApplicationService::create(&private_key, &db, form.application_id, &plain_text_password, form.personal_id_number.clone()) .await .map_err(to_custom_error)?; @@ -113,25 +109,24 @@ pub async fn create_candidate( ) } +#[allow(unused_variables)] #[get("/candidates?&")] pub async fn list_candidates( conn: Connection<'_, Db>, session: AdminAuth, field: Option, - page: Option, -) -> Result>, Custom> { + page: Option, +) -> Result>, Custom> { let db = conn.into_inner(); let private_key = session.get_private_key(); if let Some(field) = field.clone() { if !(field == "KB".to_string() || field == "IT".to_string() || field == "G") { return Err(Custom(Status::BadRequest, "Invalid field of study".to_string())); } - } - let candidates = CandidateService::list_candidates(&private_key, db, field, page) - .await - .map_err(to_custom_error)?; + let candidates = ApplicationService::list_applications(&private_key, db, field, page) + .await.map_err(to_custom_error)?; Ok( Json(candidates) @@ -164,7 +159,7 @@ 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))?; @@ -172,7 +167,7 @@ pub async fn get_candidate( let details = ApplicationService::decrypt_all_details( private_key, db, - candidate + &application ) .await .map_err(to_custom_error)?; @@ -190,14 +185,22 @@ 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)?; + + ApplicationService::delete(db, application).await.map_err(to_custom_error)?; + + let remaining_applications = Query::find_applications_by_candidate_id(db, candidate.id).await + .map_err(|e| to_custom_error(ServiceError::DbError(e)))?; + + if remaining_applications.is_empty() { + CandidateService::delete_candidate(db, candidate).await.map_err(to_custom_error)?; + } - CandidateService::delete_candidate(db, candidate) - .await - .map_err(to_custom_error) + Ok(()) } #[post("/candidate//reset_password")] @@ -206,13 +209,14 @@ pub async fn reset_candidate_password( session: AdminAuth, id: i32, ) -> Result, Custom> { + // TODO 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( Json(response) ) @@ -220,12 +224,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/routes/candidate.rs b/api/src/routes/candidate.rs index c565bed..b8a3550 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -1,11 +1,12 @@ 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; 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}; @@ -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,10 +73,21 @@ 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 response = NewCandidateResponse::from_encrypted(&private_key, candidate).await + let application: entity::application::Model = session.into(); + 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( + application.id, + applications, + &private_key, + candidate + ).await .map_err(to_custom_error)?; Ok(Json(response)) @@ -86,13 +98,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).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, candidate, &form) .await .map_err(to_custom_error)?; @@ -102,13 +115,17 @@ 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 details = ApplicationService::decrypt_all_details(private_key, db, candidate) + let details = ApplicationService::decrypt_all_details( + private_key, + db, + &application + ) .await .map(|x| Json(x)) .map_err(to_custom_error); @@ -117,12 +134,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 application: entity::application::Model = session.into(); - PortfolioService::add_cover_letter_to_cache(candidate.application, letter.into()) + PortfolioService::add_cover_letter_to_cache(application.candidate_id, letter.into()) .await .map_err(to_custom_error)?; @@ -130,10 +147,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 application: entity::application::Model = session.into(); - PortfolioService::delete_cover_letter_from_cache(candidate.application) + PortfolioService::delete_cover_letter_from_cache(application.candidate_id) .await .map_err(to_custom_error)?; @@ -142,12 +159,12 @@ pub async fn delete_cover_letter(session: CandidateAuth) -> Result<(), Custom Result<(), Custom> { - let candidate: entity::candidate::Model = session.into(); + let application: entity::application::Model = session.into(); - PortfolioService::add_portfolio_letter_to_cache(candidate.application, letter.into()) + PortfolioService::add_portfolio_letter_to_cache(application.candidate_id, letter.into()) .await .map_err(to_custom_error)?; @@ -155,10 +172,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.candidate_id) .await .map_err(to_custom_error)?; @@ -167,12 +184,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 application: entity::application::Model = session.into(); - PortfolioService::add_portfolio_zip_to_cache(candidate.application, portfolio.into()) + PortfolioService::add_portfolio_zip_to_cache(application.candidate_id, portfolio.into()) .await .map_err(to_custom_error)?; @@ -180,10 +197,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 application: entity::application::Model = session.into(); - PortfolioService::delete_portfolio_zip_from_cache(candidate.application) + PortfolioService::delete_portfolio_zip_from_cache(application.candidate_id) .await .map_err(to_custom_error)?; @@ -192,11 +209,11 @@ pub async fn delete_portfolio_zip(session: CandidateAuth) -> Result<(), Custom Result, Custom> { - let candidate: entity::candidate::Model = session.into(); + let application: entity::application::Model = session.into(); - let progress = PortfolioService::get_submission_progress(candidate.application) + let progress = PortfolioService::get_submission_progress(application.candidate_id) .await .map(|x| Json(x)) .map_err(to_custom_error); @@ -207,11 +224,12 @@ 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).await.map_err(to_custom_error)?; // TODO let submit = PortfolioService::submit(&candidate, &db).await; @@ -220,7 +238,7 @@ pub async fn submit_portfolio( // Delete on critical error if e.code() == 500 { // Cleanup - PortfolioService::delete_portfolio(candidate.application) + PortfolioService::delete_portfolio(application.id) .await .unwrap(); } @@ -232,11 +250,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 application: entity::application::Model = session.into(); - PortfolioService::delete_portfolio(candidate.application) + PortfolioService::delete_portfolio(application.candidate_id) .await .map_err(to_custom_error)?; @@ -244,11 +262,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 application: entity::application::Model = session.into(); - let file = PortfolioService::get_portfolio(candidate.application, private_key) + let file = PortfolioService::get_portfolio(application.candidate_id, private_key) .await .map_err(to_custom_error); @@ -299,8 +317,7 @@ mod tests { \"sex\": \"MALE\", \"personalIdNumber\": \"0101010000\", \"schoolName\": \"29988383\", - \"healthInsurance\": \"000\", - \"study\": \"KB\" + \"healthInsurance\": \"000\" }, \"parents\": [ { @@ -331,7 +348,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..f92b123 100644 --- a/api/src/test.rs +++ b/api/src/test.rs @@ -42,14 +42,13 @@ pub mod tests { .await .unwrap(); - ApplicationService::create_candidate_with_parent( + ApplicationService::create( + &"".to_string(), db, APPLICATION_ID, &CANDIDATE_PASSWORD.to_string(), - PERSONAL_ID_NUMBER.to_string(), - ) - .await - .unwrap(); + PERSONAL_ID_NUMBER.to_string()) + .await.unwrap(); } pub fn test_client() -> &'static Mutex { diff --git a/core/Cargo.toml b/core/Cargo.toml index 53c30e7..4e94eec 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "portfolio-core" -version = "1.0.0" +version = "2.0.0" edition = "2021" publish = false diff --git a/core/src/crypto.rs b/core/src/crypto.rs index ae36c4a..0d2ded8 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -4,6 +4,8 @@ use argon2::{ Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier, }; use async_compat::CompatExt; +use base64::Engine; +use base64::engine::general_purpose::STANDARD as base64; use futures::io::{AsyncReadExt, AsyncWriteExt}; use rand::Rng; use secrecy::ExposeSecret; @@ -123,14 +125,14 @@ pub async fn encrypt_password( }) .await??; - Ok(base64::encode(hash)) + Ok(base64.encode(hash)) } pub async fn decrypt_password( password_cipher_text: String, key: String, ) -> Result { - let input = base64::decode(password_cipher_text)?; + let input = base64.decode(password_cipher_text)?; let plain = tokio::task::spawn_blocking(move || { let aes_key_nonce = convert_key_aes256(&key); @@ -164,7 +166,7 @@ pub async fn encrypt_password_age( encrypt_writer.close().await?; - Ok(base64::encode(encrypt_buffer)) + Ok(base64.encode(encrypt_buffer)) } #[deprecated(note = "Too slow, use AES instead")] @@ -172,7 +174,7 @@ pub async fn decrypt_password_age( password_encrypted: &str, key: &str, ) -> Result { - let encrypted = base64::decode(password_encrypted)?; + let encrypted = base64.decode(password_encrypted)?; let decryptor = match age::Decryptor::new_async(&encrypted[..]).await? { age::Decryptor::Passphrase(d) => d, @@ -263,14 +265,14 @@ pub async fn encrypt_password_with_recipients( ) .await?; - Ok(base64::encode(encrypt_buffer)) + Ok(base64.encode(encrypt_buffer)) } pub async fn decrypt_password_with_private_key( password_encrypted: &str, key: &str, ) -> Result { - let encrypted = base64::decode(password_encrypted)?; + let encrypted = base64.decode(password_encrypted)?; let mut decrypt_buffer = Vec::new(); @@ -338,6 +340,9 @@ pub async fn decrypt_file_with_private_key_as_buffer>( #[cfg(test)] mod tests { + use base64::Engine; + use base64::engine::general_purpose::STANDARD as base64; + #[test] fn test_random_12_char_string() { for _ in 0..1000 { @@ -406,7 +411,7 @@ mod tests { .await .unwrap(); - assert!(base64::decode(encrypted).is_ok()); + assert!(base64.decode(encrypted).is_ok()); } #[tokio::test] @@ -433,7 +438,7 @@ mod tests { #[allow(deprecated)] let encrypted = super::encrypt_password_age(PASSWORD, KEY).await.unwrap(); - assert!(base64::decode(encrypted).is_ok()); + assert!(base64.decode(encrypted).is_ok()); } #[tokio::test] @@ -466,7 +471,7 @@ mod tests { .await .unwrap(); - assert!(base64::decode(encrypted).is_ok()); + assert!(base64.decode(encrypted).is_ok()); } #[tokio::test] @@ -480,7 +485,7 @@ mod tests { .await .unwrap(); - assert!(base64::decode(encrypted).is_ok()); + assert!(base64.decode(encrypted).is_ok()); } #[tokio::test] diff --git a/core/src/database/mutation/admin_session.rs b/core/src/database/mutation/admin_session.rs index e182381..d9fd29c 100644 --- a/core/src/database/mutation/admin_session.rs +++ b/core/src/database/mutation/admin_session.rs @@ -13,7 +13,7 @@ impl Mutation { ) -> Result { admin_session::ActiveModel { id: Set(random_uuid), - admin_id: Set(Some(admin_id)), + admin_id: Set(admin_id), ip_address: Set(ip_addr), created_at: Set(Utc::now().naive_local()), expires_at: Set(Utc::now() diff --git a/core/src/database/mutation/application.rs b/core/src/database/mutation/application.rs new file mode 100644 index 0000000..115aac3 --- /dev/null +++ b/core/src/database/mutation/application.rs @@ -0,0 +1,65 @@ +use ::entity::application; +use log::{info, warn}; +use sea_orm::{DbConn, DbErr, Set, ActiveModelTrait, IntoActiveModel, DeleteResult, ModelTrait}; + +use crate::{Mutation, models::candidate::FieldOfStudy}; + +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 field_of_study = FieldOfStudy::from(application_id); + let insert = application::ActiveModel { + id: Set(application_id), + field_of_study: Set(field_of_study.into()), + 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()), + } + .insert(db) + .await?; + + info!("APPLICATION {} CREATED", application_id); + Ok(insert) + } + + pub async fn delete_application( + db: &DbConn, + application: application::Model, + ) -> Result { + let application_id = application.id; + let delete = application.delete(db).await?; + + warn!("APPLICATION {} DELETED", application_id); + Ok(delete) + } + + 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..5f53073 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; 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,47 +18,29 @@ impl Mutation { .insert(db) .await?; - info!("CANDIDATE {} CREATED", application_id); - Ok(insert) + info!("CANDIDATE {} CREATED", candidate.id); + Ok(candidate) } pub async fn delete_candidate( db: &DbConn, candidate: candidate::Model, ) -> Result { - let application = candidate.application; + let application = candidate.id; let delete = candidate.delete(db).await?; warn!("CANDIDATE {} DELETED", application); 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, + candidate: candidate::Model, enc_candidate: EncryptedCandidateDetails, + encrypted_by_id: i32, ) -> Result { - let application = user.application; - 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())); @@ -77,10 +51,10 @@ 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.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()); @@ -90,6 +64,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)] @@ -103,20 +91,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.id) .await .unwrap(); assert!(candidate.is_some()); @@ -126,15 +108,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,12 +120,12 @@ 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, 1).await.unwrap(); - let candidate = Query::find_candidate_by_id(&db, APPLICATION_ID) + let candidate = Query::find_candidate_by_id(&db, candidate.id) .await .unwrap().unwrap(); - assert!(candidate.study.is_some()); + assert!(candidate.name.is_some()); } } 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..6959cb6 100644 --- a/core/src/database/mutation/parent.rs +++ b/core/src/database/mutation/parent.rs @@ -6,7 +6,7 @@ use sea_orm::*; impl Mutation { pub async fn create_parent(db: &DbConn, application_id: i32) -> Result { parent::ActiveModel { - application: Set(application_id), + candidate_id: Set(application_id), created_at: Set(chrono::offset::Local::now().naive_local()), updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() @@ -49,43 +49,31 @@ 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(); + 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()); + let parents = Query::find_candidate_parents(&db, &candidate).await.unwrap(); + assert!(parents.get(0).is_some()); } #[tokio::test] 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.id).await.unwrap(); let encrypted_details: EncryptedApplicationDetails = EncryptedApplicationDetails::new( &APPLICATION_DETAILS.lock().unwrap().clone(), @@ -94,15 +82,14 @@ mod tests { .await .unwrap(); - let parent = Mutation::add_parent_details(&db, parent, encrypted_details.parents[0].clone()) + Mutation::add_parent_details(&db, parent, encrypted_details.parents[0].clone()) .await .unwrap(); - let parent = Query::find_parent_by_id(&db, parent.id) + let parents = Query::find_candidate_parents(&db, &candidate) .await - .unwrap() .unwrap(); - assert!(parent.surname.is_some()); + assert!(parents[0].surname.is_some()); } } diff --git a/core/src/database/mutation/session.rs b/core/src/database/mutation/session.rs index e4d11aa..6f2ba4a 100644 --- a/core/src/database/mutation/session.rs +++ b/core/src/database/mutation/session.rs @@ -14,7 +14,7 @@ impl Mutation { ) -> Result { session::ActiveModel { id: Set(random_uuid), - candidate_id: Set(Some(candidate_id)), + candidate_id: Set(candidate_id), ip_address: Set(ip_addr), created_at: Set(Utc::now().naive_local()), expires_at: Set(Utc::now() diff --git a/core/src/database/query/application.rs b/core/src/database/query/application.rs new file mode 100644 index 0000000..1f5334e --- /dev/null +++ b/core/src/database/query/application.rs @@ -0,0 +1,90 @@ +use entity::{application, candidate}; +use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait}; + +const PAGE_SIZE: u64 = 20; + +#[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, +} + +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 + } + + pub async fn list_applications( + db: &DbConn, + field_of_study: Option, + page: Option, + ) -> Result, DbErr> { + let select = application::Entity::find(); + let query = if let Some(field) = field_of_study { + select.filter(application::Column::FieldOfStudy.eq(field)) + } else { + select + } + .order_by(application::Column::Id, sea_orm::Order::Asc) + .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") + .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_applications_compact( + db: &DbConn, + ) -> Result, DbErr> { + application::Entity::find() + .join(JoinType::InnerJoin, application::Relation::Candidate.def()) + .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 38d112c..461a802 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, @@ -38,35 +44,11 @@ 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::Application, 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> { Candidate::find() - .order_by(candidate::Column::Application, Order::Asc) + .order_by(candidate::Column::Id, Order::Asc) .all(db) .await } @@ -75,13 +57,22 @@ 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 } - + + 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)] @@ -97,10 +88,7 @@ mod tests { async fn test_find_candidate_by_id() { 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()), + 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()), @@ -110,7 +98,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/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..b512b4b 100644 --- a/core/src/database/query/parent.rs +++ b/core/src/database/query/parent.rs @@ -2,23 +2,12 @@ use entity::candidate; use entity::parent; use entity::parent::Model; -use entity::parent::Entity; use sea_orm::ModelTrait; use sea_orm::{DbConn, DbErr}; -use sea_orm::EntityTrait; use crate::Query; impl Query { - #[deprecated(note = "Use find_candidate_parents instead")] - pub async fn find_parent_by_id( - db: &DbConn, - id: i32, - ) -> Result, DbErr> { - - Entity::find_by_id(id).one(db).await - } - pub async fn find_candidate_parents( db: &DbConn, candidate: &candidate::Model, @@ -42,13 +31,10 @@ mod tests { async fn test_find_parent_by_id() { let db = get_memory_sqlite_connection().await; - const APPLICATION_ID: i32 = 103158; + const CANDIDATE_ID: i32 = 103158; candidate::ActiveModel { - application: Set(APPLICATION_ID), - code: Set("test".to_string()), - public_key: Set("test".to_string()), - private_key: Set("test".to_string()), + id: Set(CANDIDATE_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()), @@ -58,7 +44,7 @@ mod tests { .await .unwrap(); let parent = parent::ActiveModel { - application: Set(APPLICATION_ID), + candidate_id: Set(CANDIDATE_ID), created_at: Set(chrono::offset::Local::now().naive_local()), updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() @@ -67,7 +53,7 @@ mod tests { .await .unwrap(); - let parent = Query::find_candidate_by_id(&db, parent.application) + let parent = Query::find_candidate_by_id(&db, parent.candidate_id) .await .unwrap(); assert!(parent.is_some()); diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index a9223fc..ab140c1 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::{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,23 +38,25 @@ impl Query { #[cfg(test)] mod tests { - use entity::{session, admin, candidate, admin_session}; + 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; + use crate::{Query}; #[tokio::test] async fn test_find_session_by_uuid() { let db = get_memory_sqlite_connection().await; + let (application, _, _) = put_user_data(&db).await; let session = session::ActiveModel { id: Set(Uuid::new_v4()), + candidate_id: Set(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()), - updated_at: Set(chrono::offset::Local::now().naive_local()), - ..Default::default() + updated_at: Set(chrono::offset::Local::now().naive_local()) } .insert(&db) .await @@ -70,23 +72,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(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()), @@ -115,7 +105,7 @@ mod tests { admin_session::ActiveModel { id: Set(Uuid::new_v4()), - admin_id: Set(Some(ADMIN_ID)), + admin_id: Set(ADMIN_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 +116,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/error.rs b/core/src/error.rs index 72bbca0..5273c4c 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -18,8 +18,18 @@ pub enum ServiceError { UserAlreadyExists, #[error("Candidate not found")] CandidateNotFound, + #[error("Resource is locked")] + Locked, + #[error("Too many applications")] + TooManyApplications, + #[error("Too many fields for one person")] + TooManyFieldsForOnePerson, + #[error("Internal server error")] + InternalServerError, #[error("Parrent not found")] ParentNotFound, + #[error("Invalid date")] + InvalidDate, #[error("Database error")] DbError(#[from] sea_orm::DbErr), #[error("Too many parents")] @@ -69,7 +79,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,7 +89,12 @@ impl ServiceError { ServiceError::CandidateNotFound => 404, ServiceError::IncompletePortfolio => 406, ServiceError::UserAlreadyExists => 409, + ServiceError::Locked => 423, + ServiceError::TooManyFieldsForOnePerson => 409, + ServiceError::TooManyApplications => 409, // 500 + ServiceError::InternalServerError => 500, + ServiceError::InvalidDate => 500, ServiceError::ParentNotFound => 500, ServiceError::DbError(_) => 500, ServiceError::UserNotFoundBySessionId => 500, diff --git a/core/src/models/application.rs b/core/src/models/application.rs new file mode 100644 index 0000000..aac9c2f --- /dev/null +++ b/core/src/models/application.rs @@ -0,0 +1,69 @@ +use serde::{Serialize, Deserialize}; + +use crate::{database::query::application::ApplicationCandidateJoin, 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, +} + +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(), + candidate_id: c.candidate_id, + } + ) + } +} + +/// CSV export (admin endpoint) +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ApplicationRow { + pub application: i32, + pub name: Option, + pub surname: Option, + pub birthplace: Option, + pub birthdate: Option, + pub address: Option, + pub telephone: Option, + pub citizenship: Option, + pub email: Option, + pub sex: Option, + pub personal_identification_number: Option, + pub school_name: Option, + pub health_insurance: Option, + + pub parent_name: Option, + pub parent_surname: Option, + pub parent_telephone: Option, + pub parent_email: Option, + + pub second_parent_name: Option, + pub second_parent_surname: Option, + pub second_parent_telephone: Option, + pub second_parent_email: Option, +} \ No newline at end of file diff --git a/core/src/models/candidate.rs b/core/src/models/candidate.rs index b121094..37cd3e9 100644 --- a/core/src/models/candidate.rs +++ b/core/src/models/candidate.rs @@ -1,18 +1,50 @@ use chrono::NaiveDate; -use entity::candidate; -use sea_orm::FromQueryResult; -use serde::{Serialize, Deserialize}; +use entity::{application, candidate}; +use serde::{Deserialize, Serialize}; -use crate::{error::ServiceError, database::query::candidate::CandidateResult, services::portfolio_service::SubmissionProgress}; +use crate::{ + error::ServiceError, +}; -use super::candidate_details::EncryptedString; +use super::candidate_details::{EncryptedString, EncryptedCandidateDetails}; + +pub enum FieldOfStudy { + G, + IT, + KB, +} + +impl Into for FieldOfStudy { + fn into(self) -> String { + match self { + FieldOfStudy::G => "G".to_string(), + FieldOfStudy::IT => "IT".to_string(), + FieldOfStudy::KB => "KB".to_string(), + } + } +} + +impl From for FieldOfStudy { + fn from(id: i32) -> Self { + match &id.to_string().as_str()[0..3] { + "101" => FieldOfStudy::G, + "102" => FieldOfStudy::IT, + "103" => FieldOfStudy::KB, + _ => panic!("Invalid field of study id"), // TODO: handle using TryFrom + } + } +} /// Minimal candidate response containing database only not null fields #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NewCandidateResponse { - pub application_id: i32, + pub current_application: i32, + pub applications: Vec, pub personal_id_number: String, + pub details_filled: bool, + pub encrypted_by: Option, + pub field_of_study: String, } /// Create candidate (admin endpoint) @@ -25,19 +57,6 @@ pub struct CreateCandidateResponse { pub password: String, } -/// List candidates (admin endpoint) -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BaseCandidateResponse { - pub application_id: i32, - pub name: String, - pub surname: String, - pub email: String, - pub telephone: String, - pub study: String, - pub progress: SubmissionProgress, -} - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "camelCase")] pub struct CandidateDetails { @@ -50,7 +69,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, @@ -73,69 +91,27 @@ pub struct ApplicationDetails { pub parents: Vec, } -/// CSV export (admin endpoint) -#[derive(FromQueryResult, Serialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct Row { - pub application: i32, - pub name: Option, - pub surname: Option, - pub birthplace: Option, - pub birthdate: Option, - pub address: Option, - pub telephone: Option, - pub citizenship: Option, - pub email: Option, - pub sex: Option, - pub study: Option, - pub personal_identification_number: Option, - pub school_name: Option, - pub health_insurance: Option, - - pub parent_name: Option, - pub parent_surname: Option, - pub parent_telephone: Option, - pub parent_email: Option, - - pub second_parent_name: Option, - pub second_parent_surname: Option, - pub second_parent_telephone: Option, - pub second_parent_email: Option, -} - impl NewCandidateResponse { - pub async fn from_encrypted(private_key: &String, c: candidate::Model) -> Result { - let id_number = EncryptedString::from(c.personal_identification_number).decrypt(private_key).await?; - Ok( - Self { - application_id: c.application, - personal_id_number: id_number, - } - ) - } -} - -impl BaseCandidateResponse { pub async fn from_encrypted( + current_application: i32, + applications: Vec, private_key: &String, - c: CandidateResult, - progress: Option, + c: candidate::Model, ) -> 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?; + let field_of_study = FieldOfStudy::from(current_application).into(); + let id_number = EncryptedString::from(c.personal_identification_number.to_owned()) + .decrypt(private_key) + .await?; + let applications = applications.iter().map(|a| a.id).collect(); + let encrypted_details = EncryptedCandidateDetails::from(&c); - 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), - } - ) + Ok(Self { + current_application, + applications, + personal_id_number: id_number, + details_filled: encrypted_details.is_filled(), + encrypted_by: c.encrypted_by_id, + field_of_study, + }) } } \ No newline at end of file diff --git a/core/src/models/candidate_details.rs b/core/src/models/candidate_details.rs index c11ab68..65fd311 100644 --- a/core/src/models/candidate_details.rs +++ b/core/src/models/candidate_details.rs @@ -3,9 +3,9 @@ use chrono::NaiveDate; use entity::{candidate, parent}; use futures::future; -use crate::{crypto, models::candidate::{Row, ApplicationDetails}, error::ServiceError}; +use crate::{crypto, models::candidate::{ApplicationDetails}, error::ServiceError, utils::date::parse_naive_date_from_opt_str}; -use super::candidate::{CandidateDetails, ParentDetails}; +use super::{candidate::{CandidateDetails, ParentDetails}, application::ApplicationRow}; pub const NAIVE_DATE_FMT: &str = "%Y-%m-%d"; @@ -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()), } ) } @@ -173,7 +171,7 @@ impl EncryptedCandidateDetails { name: d.0.unwrap_or_default(), surname: d.1.unwrap_or_default(), birthplace: d.2.unwrap_or_default(), - birthdate: NaiveDate::parse_from_str(&d.3.unwrap_or_default(), NAIVE_DATE_FMT).unwrap_or(NaiveDate::from_ymd(1, 1, 1)), + birthdate: parse_naive_date_from_opt_str(d.3, NAIVE_DATE_FMT)?, address: d.4.unwrap_or_default(), telephone: d.5.unwrap_or_default(), citizenship: d.6.unwrap_or_default(), @@ -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(), } ) } @@ -196,9 +193,8 @@ impl EncryptedCandidateDetails { self.telephone.is_some() && self.citizenship.is_some() && self.email.is_some() && - self.sex.is_some() && - self.personal_id_number.is_some() && - self.study.is_some() + // self.sex.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(), } } } @@ -336,11 +331,11 @@ impl From<(&candidate::Model, Vec)> for EncryptedApplicationDetai } } -impl TryFrom for EncryptedApplicationDetails { +impl TryFrom for EncryptedApplicationDetails { type Error = ServiceError; fn try_from( - cp: Row, + cp: ApplicationRow, ) -> Result { Ok(EncryptedApplicationDetails { candidate: EncryptedCandidateDetails { @@ -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,8 +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"); assert_eq!(parent.surname, "parent_surname"); @@ -510,7 +501,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 (_, candidate, parents) = put_user_data(&db).await; let encrypted_details = EncryptedApplicationDetails::try_from((&candidate, parents)).unwrap(); 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/admin_service.rs b/core/src/services/admin_service.rs index 059848c..034cb56 100644 --- a/core/src/services/admin_service.rs +++ b/core/src/services/admin_service.rs @@ -56,7 +56,7 @@ impl AuthenticableTrait for AdminService { return Err(ServiceError::ExpiredSession); } - let admin = Query::find_admin_by_id(db, session.admin_id.unwrap()) + let admin = Query::find_admin_by_id(db, session.admin_id) .await? .ok_or(ServiceError::CandidateNotFound)?; @@ -104,15 +104,35 @@ impl AuthenticableTrait for AdminService { } #[cfg(test)] -mod admin_tests { - use chrono::Local; +pub mod admin_tests { + use chrono::{Local, Utc}; use entity::admin; use sea_orm::{Set, ActiveModelTrait}; - + use crate::{utils::db::get_memory_sqlite_connection, error::ServiceError}; - + use super::*; - + + pub async fn create_admin(db: &DbConn) -> admin::Model { + let password = "admin".to_string(); + let (pubkey, priv_key) = crypto::create_identity(); + let enc_priv_key = crypto::encrypt_password(priv_key, password).await.unwrap(); + + let admin = admin::ActiveModel { + name: Set("admin".to_string()), + public_key: Set(pubkey), + private_key: Set(enc_priv_key), + password: Set("admin".to_string()), + created_at: Set(Utc::now().naive_utc()), + updated_at: Set(Utc::now().naive_utc()), + ..Default::default() + } + .insert(db) + .await + .unwrap(); + + admin + } #[tokio::test] async fn test_admin_login() -> Result<(), ServiceError> { diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 63b62ea..b791425 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -1,35 +1,208 @@ -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, CreateCandidateResponse}, candidate_details::EncryptedString, auth::AuthenticableTrait, application::ApplicationResponse}, 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( + admin_private_key: &String, db: &DbConn, application_id: i32, plain_text_password: &String, personal_id_number: String, - ) -> Result<(candidate::Model, parent::Model), ServiceError> { - Ok( - ( - CandidateService::create(db, application_id, plain_text_password, personal_id_number).await?, - ParentService::create(db, application_id).await? + ) -> 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 (candidate, enc_personal_id_number) = Self::find_or_create_candidate_with_personal_id( + application_id, + admin_private_key, + db, + personal_id_number, + &pubkey, + ).await?; + + + let application = Mutation::create_application( + db, + application_id, + candidate.id, + hashed_password, + enc_personal_id_number.to_string(), + pubkey, + encrypted_priv_key, + ).await?; + + let applications = Query::find_applications_by_candidate_id(db, candidate.id).await?; + if applications.len() >= 3 { + for application in applications { + ApplicationService::delete(db, application).await?; + } + return Err(ServiceError::InternalServerError); + } + + Ok(application) + } + + async fn find_or_create_candidate_with_personal_id( + application_id: i32, + admin_private_key: &String, + db: &DbConn, + personal_id_number: String, + 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 {( + 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(); + + if let Some((candidate_id, _)) = found_ids.first() { + Ok( + Self::find_linkable_candidate(db, + application_id, + *candidate_id, + pubkey, + personal_id_number + ).await? ) + } else { + let recipients = get_recipients(db, pubkey).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(), + ) + ) + } + } + + async fn find_linkable_candidate( + db: &DbConn, + new_application_id: i32, + candidate_id: i32, + pubkey: &String, + personal_id_number: String, + ) -> Result<(candidate::Model, String), ServiceError> { + let candidate = Query::find_candidate_by_id(db, candidate_id) + .await? + .ok_or(ServiceError::CandidateNotFound)?; + + let linked_applications = Query::find_applications_by_candidate_id(db, candidate.id).await?; + + if linked_applications.len() > 1 { + return Err(ServiceError::TooManyApplications); + } + + let linked_application = linked_applications.first().ok_or(ServiceError::CandidateNotFound)?;//TODO + + if linked_application.id.to_string()[0..3] == new_application_id.to_string()[0..3] { + return Err(ServiceError::TooManyFieldsForOnePerson); + } + + let mut recipients = Query::get_all_admin_public_keys(db).await?; + recipients.append(&mut vec![linked_application.public_key.to_owned(), pubkey.to_owned()]); + + + 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!("APPLICATIONS {} AND {} ARE LINKED (CANDIDATE {})", new_application_id, linked_application.id, candidate.id); + Ok( + (candidate, enc_personal_id_number.to_string()) ) } + pub async fn delete(db: &DbConn, application: application::Model) -> Result<(), ServiceError> { + Mutation::delete_application(db, application).await?; + Ok(()) + } + + 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 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, + application: &application::Model, candidate: candidate::Model, form: &ApplicationDetails, ) -> Result<(candidate::Model, Vec), ServiceError> { + let mut recipients = Query::get_all_admin_public_keys(db).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 recipients = get_recipients(db, &candidate.public_key).await?; - let candidate = CandidateService::add_candidate_details(db, candidate, &form.candidate, &recipients).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( ( @@ -42,8 +215,10 @@ impl ApplicationService { pub async fn decrypt_all_details( private_key: String, db: &DbConn, - candidate: candidate::Model, + application: &application::Model, ) -> Result { + let candidate = ApplicationService::find_related_candidate(db, application).await?; + let parents = Query::find_candidate_parents(db, &candidate).await?; let enc_details = EncryptedApplicationDetails::from((&candidate, parents)); @@ -52,7 +227,268 @@ impl ApplicationService { } else { Err(ServiceError::Forbidden) } - } - + + pub async fn list_applications( + private_key: &String, + db: &DbConn, + field_of_study: Option, + page: Option, + + ) -> Result, ServiceError> { + let applications = Query::list_applications(db, field_of_study, page).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( + 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 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?; + + 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, &application, 0).await?; + let application = Mutation::update_application_password_and_keys(db, + application, + 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(application.personal_id_number.clone()) + .decrypt(&admin_private_key) + .await?; + + let applications = Query::find_applications_by_candidate_id(db, candidate.id).await?; + let mut recipients = vec![]; + let mut admin_public_keys = Query::get_all_admin_public_keys(db).await?; + recipients.append(&mut admin_public_keys); + recipients.append(&mut applications.iter().map(|a| a.public_key.to_owned()).collect()); + + let dec_details = EncryptedApplicationDetails::from((&candidate, parents.clone())) + .decrypt(admin_private_key).await?; + + let enc_details = EncryptedApplicationDetails::new(&dec_details, recipients).await?; + + let candidate = Mutation::update_personal_id(db, + candidate, + &enc_details.candidate.personal_id_number.to_owned() + .ok_or(ServiceError::CandidateDetailsNotSet)?.to_string() + ).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?; + } + + 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) + .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(()) + } +} + +#[cfg(test)] +mod application_tests { + use crate::{services::{application_service::ApplicationService, candidate_service::tests::put_user_data}, utils::db::get_memory_sqlite_connection, crypto, models::auth::AuthenticableTrait}; + use crate::services::admin_service::admin_tests::create_admin; + + #[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 (application, _, _) = put_user_data(&db).await; + + let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); + + assert!( + ApplicationService::login(&db, application.id, "test".to_string(), "127.0.0.1".to_string()).await.is_ok() + ); + + let new_password = ApplicationService::reset_password(private_key, &db, application.id).await.unwrap().password; + + assert!( + ApplicationService::login(&db, application.id, "test".to_string(), "127.0.0.1".to_string()).await.is_err() + ); + + assert!( + ApplicationService::login(&db, application.id, 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(&"".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]) + .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..661d614 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,19 +1,13 @@ -use async_trait::async_trait; -use chrono::Duration; -use entity::{candidate, session}; -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, }; -use super::{session_service::SessionService, portfolio_service::PortfolioService}; - - -const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; +use super::{portfolio_service::PortfolioService}; pub struct CandidateService; @@ -25,106 +19,22 @@ 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.id).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?; + PortfolioService::delete_candidate_root(candidate.id).await?; Mutation::delete_candidate(db, candidate).await?; Ok(()) @@ -135,297 +45,73 @@ 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) } - - 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 - } - - 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::services::admin_service::admin_tests::create_admin; 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::{candidate, parent, admin}; + use entity::{application, candidate, parent}; 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() { + 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, None, None).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, None, None).await.unwrap(); assert_eq!(candidates.len(), 1); } - #[tokio::test] - async fn test_encrypt_decrypt_private_key_with_passphrase() { - let db = get_memory_sqlite_connection().await; + #[cfg(test)] + pub async fn put_user_data(db: &DbConn) -> (application::Model, candidate::Model, Vec) { + use crate::{models::candidate_details::tests::APPLICATION_DETAILS, services::parent_service::ParentService}; 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; - use sea_orm::{Set, ActiveModelTrait}; - - let password = "admin".to_string(); - let (pubkey, priv_key) = crypto::create_identity(); - let enc_priv_key = crypto::encrypt_password(priv_key, password).await.unwrap(); - - let admin = admin::ActiveModel { - name: Set("admin".to_string()), - public_key: Set(pubkey), - private_key: Set(enc_priv_key), - password: Set("admin".to_string()), - created_at: Set(Utc::now().naive_utc()), - updated_at: Set(Utc::now().naive_utc()), - ..Default::default() - } - .insert(db) - .await - .unwrap(); - - admin - } - - #[cfg(test)] - pub async fn put_user_data(db: &DbConn) -> (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( + &"".to_string(), + db, APPLICATION_ID, &plain_text_password, - "".to_string(), - ) - .await - .ok() - .unwrap(); + "0000001111".to_string() + ).await.unwrap(); + + let candidate= ApplicationService::find_related_candidate(db, &application).await.unwrap(); + ParentService::create(db, candidate.id).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, candidate, &form) .await .unwrap(); ( + application, candidate, parents, ) @@ -434,7 +120,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 (_, candidate, parents) = put_user_data(&db).await; assert!(candidate.name.is_some()); assert!(parents[0].name.is_some()); } @@ -443,9 +129,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..f07ba2f 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?; @@ -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}, candidate_details::EncryptedApplicationDetails}, services::{candidate_service::{CandidateService, tests::put_user_data}, application_service::ApplicationService, parent_service::ParentService}, crypto}; pub static APPLICATION_DETAILS_TWO_PARENTS: Lazy> = Lazy::new(|| Mutex::new(ApplicationDetails { @@ -73,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(), @@ -93,34 +90,27 @@ 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.id).await.unwrap(); + super::ParentService::create(&db, candidate.id).await.unwrap(); } #[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( - &db, - 103101, - &plain_text_password, - "".to_string(), - ) - .await - .ok() - .unwrap(); + // let application = ApplicationService::create(&"".to_string(), &db, 103100, &plain_text_password, "".to_string()).await.unwrap(); + let (application, candidate, _) = put_user_data(&db).await; - ParentService::create(&db, 103101).await.unwrap(); + ParentService::create(&db, candidate.id).await.unwrap(); 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, 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) @@ -136,8 +126,7 @@ mod tests { assert_eq!(dec_details.candidate.citizenship, form.candidate.citizenship); assert_eq!(dec_details.candidate.email, form.candidate.email); assert_eq!(dec_details.candidate.sex, form.candidate.sex); - assert_eq!(dec_details.candidate.personal_id_number, form.candidate.personal_id_number); - assert_eq!(dec_details.candidate.study, form.candidate.study); + assert_eq!(dec_details.candidate.personal_id_number, "0000001111".to_string()); assert_eq!(dec_details.parents.len(), form.parents.len()); for i in 0..dec_details.parents.len() { @@ -146,8 +135,5 @@ mod tests { assert_eq!(dec_details.parents[i].telephone, form.parents[i].telephone); assert_eq!(dec_details.parents[i].email, form.parents[i].email); } - - - } } \ No newline at end of file diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index 6ae6f4b..19cede7 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 @@ -269,7 +259,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> { - 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 +267,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); @@ -306,11 +296,15 @@ 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 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]; - recipients.append(&mut admin_public_keys_refrence); + + let mut recipients = vec![]; + recipients.append(&mut admin_public_keys.iter().map(|s| &**s).collect()); + recipients.append(&mut applications_pubkeys.iter().map(|s| &**s).collect()); let final_path = path.join(FileType::PortfolioZip.as_str()); @@ -420,18 +414,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.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(); } @@ -620,14 +613,14 @@ 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 (_, candidate, _) = put_user_data(&db).await; + + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).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.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(&candidate, &db).await.unwrap(); @@ -639,20 +632,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 (_, 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.id).await; + + 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(&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.id).await.unwrap(); assert!(!tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); @@ -662,32 +655,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 (_, candidate, _) = put_user_data(&db).await; + let (temp_dir, _, _) = create_data_store_temp_dir(candidate.id).await; + + 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(&candidate, &db).await.unwrap(); - assert!(PortfolioService::is_portfolio_submitted(APPLICATION_ID).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(APPLICATION_ID).await; + let (temp_dir, application_dir, _) = create_data_store_temp_dir(candidate.id).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.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(&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.id).await); clear_data_store_temp_dir(temp_dir).await; } @@ -695,22 +688,22 @@ 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.id).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.id, vec![0]) .await .unwrap(); - PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]) + PortfolioService::add_portfolio_letter_to_cache(candidate.id, vec![0]) .await .unwrap(); - PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]) + PortfolioService::add_portfolio_zip_to_cache(candidate.id, vec![0]) .await .unwrap(); @@ -718,7 +711,7 @@ mod tests { .await .unwrap(); - PortfolioService::get_portfolio(APPLICATION_ID, private_key) + PortfolioService::get_portfolio(candidate.id, private_key) .await .unwrap(); diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index 58cd477..30321d7 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(&"".to_string(), &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(&"".to_string(), &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(&"".to_string(), &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/csv.rs b/core/src/utils/csv.rs index 6d6cef9..ce6827f 100644 --- a/core/src/utils/csv.rs +++ b/core/src/utils/csv.rs @@ -1,7 +1,12 @@ -use sea_orm::{DbConn}; -use crate::{error::ServiceError, models::candidate_details::{EncryptedApplicationDetails}, Query, models::candidate::{Row, ApplicationDetails}}; +use crate::{ + error::ServiceError, + models::candidate_details::EncryptedApplicationDetails, + models::{application::ApplicationRow, candidate::ApplicationDetails}, + Query, services::application_service::ApplicationService, +}; +use sea_orm::DbConn; -impl From<(i32, ApplicationDetails)> for Row { +impl From<(i32, ApplicationDetails)> for ApplicationRow { fn from((application, d): (i32, ApplicationDetails)) -> Self { let c = d.candidate; Self { @@ -15,7 +20,6 @@ impl From<(i32, ApplicationDetails)> for Row { citizenship: Some(c.citizenship), email: Some(c.email), sex: Some(c.sex), - study: Some(c.study), health_insurance: Some(c.health_insurance), school_name: Some(c.school_name), personal_identification_number: Some(c.personal_id_number), @@ -33,33 +37,29 @@ impl From<(i32, ApplicationDetails)> for Row { } } -pub async fn export( - db: &DbConn, - private_key: String, -) -> Result, ServiceError> { +pub async fn export(db: &DbConn, private_key: String) -> Result, ServiceError> { let mut wtr = csv::Writer::from_writer(vec![]); - let candidates_with_parents = Query::list_candidates_full(&db).await?; - for candidate in candidates_with_parents { - let application = candidate.application; + let applications = Query::list_applications_compact(&db).await?; + for application in applications { + let candidate = ApplicationService::find_related_candidate(db, &application).await?; let parents = Query::find_candidate_parents(db, &candidate).await?; - let row: Row = match EncryptedApplicationDetails::try_from((&candidate, parents)) { - Ok(d) => Row::from( - d - .decrypt(private_key.to_string()) + let row: ApplicationRow = match EncryptedApplicationDetails::try_from((&candidate, parents)) + { + Ok(d) => ApplicationRow::from( + d.decrypt(private_key.to_string()) .await - .map(|d| (application, d))? + .map(|d| (application.id, d))?, ), - Err(_) => Row { - application, + Err(_) => ApplicationRow { + application: application.id, ..Default::default() - } + }, }; wtr.serialize(row)?; } - wtr - .into_inner() + wtr.into_inner() .map_err(|_| ServiceError::CsvIntoInnerError) -} \ No newline at end of file +} diff --git a/core/src/utils/date.rs b/core/src/utils/date.rs new file mode 100644 index 0000000..262755f --- /dev/null +++ b/core/src/utils/date.rs @@ -0,0 +1,13 @@ +use chrono::NaiveDate; + +use crate::error::ServiceError; + +pub fn parse_naive_date_from_opt_str(date: Option, fmt: &str) -> Result { + Ok( + NaiveDate::parse_from_str(&date.unwrap_or_default(), fmt) + .unwrap_or( + NaiveDate::from_ymd_opt(1, 1, 1) + .ok_or(ServiceError::InvalidDate)? + ) + ) +} \ No newline at end of file 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/core/src/utils/field_of_study.rs b/core/src/utils/field_of_study.rs new file mode 100644 index 0000000..e69de29 diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs index d498f2d..7065490 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -1,3 +1,5 @@ pub mod csv; pub mod filetype; -pub mod db; \ No newline at end of file +pub mod db; +pub mod date; +pub mod field_of_study; \ No newline at end of file diff --git a/entity/Cargo.toml b/entity/Cargo.toml index fd7c617..0d30070 100644 --- a/entity/Cargo.toml +++ b/entity/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "portfolio-entity" -version = "1.0.0" +version = "2.0.0" edition = "2021" publish = false diff --git a/entity/src/admin_session.rs b/entity/src/admin_session.rs index 1815b5d..b86b082 100644 --- a/entity/src/admin_session.rs +++ b/entity/src/admin_session.rs @@ -9,7 +9,7 @@ use crate::session_trait::UserSession; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, - pub admin_id: Option, + pub admin_id: i32, pub ip_address: String, pub created_at: DateTime, pub expires_at: DateTime, diff --git a/entity/src/application.rs b/entity/src/application.rs new file mode 100644 index 0000000..22dbd9c --- /dev/null +++ b/entity/src/application.rs @@ -0,0 +1,46 @@ +//! 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 field_of_study: String, + 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::Id", + 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..9f88ce3 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)] - pub application: i32, - pub code: String, + #[sea_orm(primary_key)] + pub id: i32, pub name: Option, pub surname: Option, pub birth_surname: Option, @@ -18,27 +17,25 @@ 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, - pub public_key: String, - pub private_key: String, + pub encrypted_by_id: Option, 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/parent.rs b/entity/src/parent.rs index 1ac42c0..5e58d0d 100644 --- a/entity/src/parent.rs +++ b/entity/src/parent.rs @@ -7,7 +7,7 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: i32, - pub application: i32, + pub candidate_id: i32, pub name: Option, pub surname: Option, pub telephone: Option, @@ -20,8 +20,8 @@ pub struct Model { pub enum Relation { #[sea_orm( belongs_to = "super::candidate::Entity", - from = "Column::Application", - to = "super::candidate::Column::Application", + from = "Column::CandidateId", + to = "super::candidate::Column::Id", on_update = "Cascade", on_delete = "Cascade" )] 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..d704cfa 100644 --- a/entity/src/session.rs +++ b/entity/src/session.rs @@ -9,7 +9,7 @@ use crate::session_trait::UserSession; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, - pub candidate_id: Option, + pub candidate_id: i32, pub ip_address: String, pub created_at: DateTime, pub expires_at: DateTime, @@ -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/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/checkbox/AccountLinkCheckBox.svelte b/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte new file mode 100644 index 0000000..6a96917 --- /dev/null +++ b/frontend/src/lib/components/checkbox/AccountLinkCheckBox.svelte @@ -0,0 +1,87 @@ + + + +
+ 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/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; + detailsFilled: boolean; + encryptedBy?: number; +} + export interface CreateCandidateLogin extends CreateCandidate { password: string; } -export const baseCandidateData = writable({ - applicationId: 0, - personalIdNumber: '' +export const baseCandidateData = writable({ + currentApplication: 0, + applications: [], + 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..f34b5da 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,23 @@ + {:else if pageIndex === 1}

{pageTexts[0]}

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

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

{pageTexts[1]}

@@ -429,7 +462,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 +538,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 +570,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 +602,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ý diff --git a/migration/Cargo.toml b/migration/Cargo.toml index d3965a7..f3ca028 100644 --- a/migration/Cargo.toml +++ b/migration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "portfolio-migration" -version = "1.0.0" +version = "2.0.0" edition = "2021" publish = false 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..77f242d 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -12,13 +12,12 @@ impl MigrationTrait for Migration { .table(Candidate::Table) .if_not_exists() .col( - ColumnDef::new(Candidate::Application) + ColumnDef::new(Candidate::Id) .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()) @@ -29,12 +28,10 @@ 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()) - .col(ColumnDef::new(Candidate::PublicKey).string().not_null()) - .col(ColumnDef::new(Candidate::PrivateKey).string().not_null()) + .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(), @@ -52,8 +49,7 @@ impl MigrationTrait for Migration { #[derive(Iden)] pub enum Candidate { Table, - Application, - Code, + Id, Name, Surname, BirthSurname, @@ -64,12 +60,10 @@ pub enum Candidate { Citizenship, Email, Sex, - Study, PersonalIdentificationNumber, SchoolName, HealthInsurance, - PublicKey, - PrivateKey, + EncryptedById, CreatedAt, UpdatedAt, } diff --git a/migration/src/m20221024_124701_create_parent.rs b/migration/src/m20221024_124701_create_parent.rs index 00d18ae..db308fd 100644 --- a/migration/src/m20221024_124701_create_parent.rs +++ b/migration/src/m20221024_124701_create_parent.rs @@ -19,7 +19,7 @@ impl MigrationTrait for Migration { .primary_key(), ) .col( - ColumnDef::new(Parent::Application) + ColumnDef::new(Parent::CandidateId) .integer() .not_null() ) @@ -45,7 +45,7 @@ impl MigrationTrait for Migration { pub enum Parent { Id, Table, - Application, + CandidateId, Name, Surname, Telephone, diff --git a/migration/src/m20221025_154422_create_session.rs b/migration/src/m20221025_154422_create_session.rs index 94cd12a..009e2df 100644 --- a/migration/src/m20221025_154422_create_session.rs +++ b/migration/src/m20221025_154422_create_session.rs @@ -18,7 +18,7 @@ impl MigrationTrait for Migration { .unique_key() .primary_key(), ) - .col(ColumnDef::new(Session::CandidateId).integer()) + .col(ColumnDef::new(Session::CandidateId).integer().not_null()) .col(ColumnDef::new(Session::IpAddress).string().not_null()) .col(ColumnDef::new(Session::CreatedAt).date_time().not_null()) .col(ColumnDef::new(Session::ExpiresAt).date_time().not_null()) 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/m20221112_112212_create_parent_candidate_fk.rs b/migration/src/m20221112_112212_create_parent_candidate_fk.rs index c390fa0..ed7f904 100644 --- a/migration/src/m20221112_112212_create_parent_candidate_fk.rs +++ b/migration/src/m20221112_112212_create_parent_candidate_fk.rs @@ -10,8 +10,8 @@ impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager.create_foreign_key(ForeignKey::create() .name("candidate_fk") - .from(Parent::Table, Parent::Application) - .to(Candidate::Table, Candidate::Application) + .from(Parent::Table, Parent::CandidateId) + .to(Candidate::Table, Candidate::Id) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade) .to_owned()).await diff --git a/migration/src/m20221221_162232_create_admin_session.rs b/migration/src/m20221221_162232_create_admin_session.rs index 73d343d..d6a0ba7 100644 --- a/migration/src/m20221221_162232_create_admin_session.rs +++ b/migration/src/m20221221_162232_create_admin_session.rs @@ -18,7 +18,7 @@ impl MigrationTrait for Migration { .not_null() .primary_key(), ) - .col(ColumnDef::new(AdminSession::AdminId).integer()) + .col(ColumnDef::new(AdminSession::AdminId).integer().not_null()) .col(ColumnDef::new(AdminSession::IpAddress).string().not_null()) .col(ColumnDef::new(AdminSession::CreatedAt).date_time().not_null()) .col(ColumnDef::new(AdminSession::ExpiresAt).date_time().not_null()) diff --git a/migration/src/m20230114_114628_create_application.rs b/migration/src/m20230114_114628_create_application.rs new file mode 100644 index 0000000..6ab88ba --- /dev/null +++ b/migration/src/m20230114_114628_create_application.rs @@ -0,0 +1,63 @@ +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::FieldOfStudy).string().not_null()) + .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?; + + 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> { + 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, + FieldOfStudy, + 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..b3bbd9f --- /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::Id) + .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 + } +}