diff --git a/Cargo.lock b/Cargo.lock index 8200336..91afc04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aead" version = "0.5.1" @@ -170,6 +176,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-compression" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +dependencies = [ + "bzip2", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "xz2", + "zstd", + "zstd-safe", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -212,6 +235,29 @@ dependencies = [ "syn", ] +[[package]] +name = "async_io_utilities" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b20cffc5590f4bf33f05f97a3ea587feba9c50d20325b401daa096b92ff7da0" +dependencies = [ + "tokio", +] + +[[package]] +name = "async_zip" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a36d43bdefc7215b2b3a97edd03b1553b7969ad76551025eedd3b913c645f6e" +dependencies = [ + "async-compression", + "async_io_utilities", + "chrono", + "crc32fast", + "thiserror", + "tokio", +] + [[package]] name = "atoi" version = "1.0.0" @@ -326,11 +372,35 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cc" version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +dependencies = [ + "jobserver", +] [[package]] name = "cfb" @@ -536,6 +606,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.6" @@ -794,6 +873,16 @@ dependencies = [ "toml", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fluent" version = "0.16.0" @@ -1373,6 +1462,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -1448,6 +1546,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1484,6 +1593,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.5" @@ -1896,6 +2014,7 @@ dependencies = [ "argon2", "async-compat", "async-tempfile", + "async_zip", "base64", "chrono", "dotenv", @@ -3552,6 +3671,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yansi" version = "0.5.1" @@ -3578,3 +3706,32 @@ dependencies = [ "syn", "synstructure", ] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.1+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" +dependencies = [ + "cc", + "libc", +] diff --git a/core/Cargo.toml b/core/Cargo.toml index 6ebc7c8..bf6c2ac 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -27,6 +27,8 @@ async-compat = "^0.2" # file identifier infer = "^0.11" +async_zip = "0.0.9" + # crypto rand = "^0.8" aes-gcm-siv = { version = "^0.11", features = ["std"] } diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index 72ef424..22ca2f1 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -19,19 +19,7 @@ mod tests { use sea_orm::{ActiveModelTrait, DbConn, Set}; use crate::Query; - - #[cfg(test)] - async fn get_memory_sqlite_connection() -> DbConn { - let base_url = "sqlite::memory:"; - let db: DbConn = Database::connect(base_url).await.unwrap(); - - let schema = Schema::new(DbBackend::Sqlite); - let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - db.execute(db.get_database_backend().build(&stmt)) - .await - .unwrap(); - db - } + use crate::util::get_memory_sqlite_connection; #[tokio::test] async fn test_find_candidate_by_id() { diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs index 2b8ed9c..3b8f361 100644 --- a/core/src/database/query/session.rs +++ b/core/src/database/query/session.rs @@ -24,24 +24,4 @@ impl Query { .all(db) .await } -} - -#[cfg(test)] -mod tests { - use entity::candidate; - use sea_orm::DbConn; - use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, Database, DbBackend, Schema}; - - #[cfg(test)] - async fn get_memory_sqlite_connection() -> DbConn { - let base_url = "sqlite::memory:"; - let db: DbConn = Database::connect(base_url).await.unwrap(); - - let schema = Schema::new(DbBackend::Sqlite); - let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - db.execute(db.get_database_backend().build(&stmt)) - .await - .unwrap(); - db - } -} +} \ No newline at end of file diff --git a/core/src/error.rs b/core/src/error.rs index cb7c05a..5aebb06 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -54,6 +54,10 @@ pub enum ServiceError { ArgonHashError(#[from] argon2::password_hash::Error), #[error("AES error")] AesError(#[from] aes_gcm_siv::Error), + #[error("Portfolio is incomplete")] + IncompletePortfolio, + #[error("Zip error")] + ZipError(#[from] async_zip::error::ZipError) } impl ServiceError { @@ -84,6 +88,9 @@ impl ServiceError { ServiceError::TokioJoinError(_) => 500, ServiceError::AesError(_) => 500, ServiceError::ArgonConfigError(_) => 500, + //TODO: Correct code + ServiceError::IncompletePortfolio => 500, + ServiceError::ZipError(_) => 500, } } } diff --git a/core/src/lib.rs b/core/src/lib.rs index c910059..322409c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,6 +4,7 @@ pub mod filetype; pub mod services; pub mod error; pub mod candidate_details; +pub mod util; pub use database::mutation::*; pub use database::query::*; diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index dd5bffb..592ce8d 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,5 +1,8 @@ +use std::path::Path; + use entity::candidate; use sea_orm::{prelude::Uuid, DbConn}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ candidate_details::EncryptedApplicationDetails, @@ -40,19 +43,17 @@ impl CandidateService { return Err(ServiceError::UserAlreadyExists); } - let Ok(hashed_password) = hash_password(plain_text_password.to_string()).await else { - return Err(ServiceError::CryptoHashFailed); - }; + let hashed_password = hash_password(plain_text_password.to_string()).await?; let (pubkey, priv_key_plain_text) = crypto::create_identity(); - let Ok(encrypted_priv_key) = crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await else { - return Err(ServiceError::CryptoEncryptFailed); - }; + let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await?; + + let hashed_personal_id_number = hash_password(personal_id_number).await?; + + // TODO: Specify root path in config? + tokio::fs::create_dir_all(Path::new(&application_id.to_string()).join("cache")).await?; - let Ok(hashed_personal_id_number) = hash_password(personal_id_number).await else { - return Err(ServiceError::CryptoHashFailed); - }; let candidate = Mutation::create_candidate( db, @@ -88,35 +89,136 @@ impl CandidateService { && candidate.study.is_some() } - pub async fn add_cover_letter(candidate_id: i32, letter: Vec) -> Result<(), ServiceError> { - // TODO + async fn write_portfolio_file( + candidate_id: i32, + data: Vec, + filename: &str, + ) -> Result<(), ServiceError> { + let cache_path = Path::new(&candidate_id.to_string()).join("cache"); + + let mut file = tokio::fs::File::create(cache_path.join(filename)).await?; + + file.write_all(&data).await?; + Ok(()) } + pub async fn add_cover_letter(candidate_id: i32, letter: Vec) -> Result<(), ServiceError> { + Self::write_portfolio_file(candidate_id, letter, "MOTIVACNI_DOPIS.pdf").await + } + pub async fn add_portfolio_letter( candidate_id: i32, letter: Vec, ) -> Result<(), ServiceError> { - // TODO - Ok(()) + Self::write_portfolio_file(candidate_id, letter, "PORTFOLIO.pdf").await } pub async fn add_portfolio_zip(candidate_id: i32, zip: Vec) -> Result<(), ServiceError> { - // TODO + Self::write_portfolio_file(candidate_id, zip, "PORTFOLIO.zip").await + } + + pub async fn is_portfolio_complete(candidate_id: i32) -> bool { + let cache_path = Path::new(&candidate_id.to_string()).join("cache"); + + tokio::fs::metadata(cache_path.join("MOTIVACNI_DOPIS.pdf")) + .await + .is_ok() + && tokio::fs::metadata(cache_path.join("PORTFOLIO.pdf")) + .await + .is_ok() + && tokio::fs::metadata(cache_path.join("PORTFOLIO.zip")) + .await + .is_ok() + } + + pub async fn submit_portfolio(candidate_id: i32, db: &DbConn) -> Result<(), ServiceError> { + let path = Path::new(&candidate_id.to_string()).to_path_buf(); + let cache_path = path.join("cache"); + + if Self::is_portfolio_complete(candidate_id).await == false { + return Err(ServiceError::IncompletePortfolio); + } + + let mut archive = tokio::fs::File::create(path.join("PORTFOLIO.zip")).await?; + + + let mut writer = async_zip::write::ZipFileWriter::new(&mut archive); + + for entry in vec!["MOTIVACNI_DOPIS.pdf", "PORTFOLIO.pdf", "PORTFOLIO.zip"] { + let mut entry_file = tokio::fs::File::open(cache_path.join(entry)) + .await?; + + let mut contents = vec![]; + + entry_file + .read_to_end(&mut contents) + .await?; + + let builder = + async_zip::ZipEntryBuilder::new(entry.to_string(), async_zip::Compression::Deflate); + + let mut entry_writer = writer + .write_entry_stream(builder) + .await?; + + // TODO: write_all_buf? + entry_writer + .write_all(&mut contents) + .await?; + } + + // TODO: Ne unwrap + writer.close().await.unwrap(); + archive.shutdown().await.unwrap(); + + let admin_public_keys = Query::get_all_admin_public_keys(db) + .await?; + + let candidate = Query::find_candidate_by_id(db, candidate_id).await? + .ok_or(ServiceError::CandidateNotFound)?; + + 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 Ok(_) = crypto::encrypt_file_with_recipients( + path.join("PORTFOLIO.zip"), + path.join("PORTFOLIO.zip"), + recipients, + ) + .await else { + return Err(ServiceError::CryptoEncryptFailed); + }; + Ok(()) } + pub async fn get_portfolio(candidate_id: i32, db: &DbConn) -> Result, ServiceError> { + let candidate = Query::find_candidate_by_id(db, candidate_id).await? + .ok_or(ServiceError::CandidateNotFound)?; + + let candidate_public_key = candidate.public_key; + + let path = Path::new(&candidate_id.to_string()).join("PORTFOLIO.zip"); + + let buffer = crypto::decrypt_file_with_private_key_as_buffer(path, &candidate_public_key).await?; + + Ok(buffer) + } + 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; - - let Ok(private_key) = private_key else { - return Err(ServiceError::CryptoDecryptFailed); - }; + let private_key = crypto::decrypt_password(private_key_encrypted, password).await?; Ok(private_key) } @@ -168,6 +270,7 @@ impl CandidateService { mod tests { use sea_orm::{Database, DbConn}; + use crate::util::get_memory_sqlite_connection; use crate::{crypto, services::candidate_service::CandidateService, Mutation}; use super::EncryptedApplicationDetails; @@ -188,32 +291,6 @@ mod tests { assert!(!CandidateService::is_application_id_valid(101)); } - #[cfg(test)] - async fn get_memory_sqlite_connection() -> DbConn { - use entity::{admin, candidate, parent}; - use sea_orm::Schema; - use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, DbBackend}; - - let base_url = "sqlite::memory:"; - let db: DbConn = Database::connect(base_url).await.unwrap(); - - let schema = Schema::new(DbBackend::Sqlite); - let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); - let stmt3: TableCreateStatement = schema.create_table_from_entity(parent::Entity); - - db.execute(db.get_database_backend().build(&stmt)) - .await - .unwrap(); - db.execute(db.get_database_backend().build(&stmt2)) - .await - .unwrap(); - db.execute(db.get_database_backend().build(&stmt3)) - .await - .unwrap(); - db - } - #[tokio::test] async fn test_encrypt_decrypt_private_key_with_passphrase() { let db = get_memory_sqlite_connection().await; diff --git a/core/src/services/session_service.rs b/core/src/services/session_service.rs index 86827da..33fdb7b 100644 --- a/core/src/services/session_service.rs +++ b/core/src/services/session_service.rs @@ -171,34 +171,9 @@ mod tests { use crate::{ crypto, - services::{session_service::SessionService, application_service::ApplicationService}, + services::{session_service::SessionService, application_service::ApplicationService}, util::get_memory_sqlite_connection, }; - #[cfg(test)] - async fn get_memory_sqlite_connection() -> DbConn { - let base_url = "sqlite::memory:"; - let db: DbConn = Database::connect(base_url).await.unwrap(); - - let schema = Schema::new(DbBackend::Sqlite); - let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); - let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity); - let stmt4: 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 - } - #[tokio::test] async fn test_create_candidate() { const SECRET: &str = "Tajny_kod"; diff --git a/core/src/util.rs b/core/src/util.rs new file mode 100644 index 0000000..f58b3b8 --- /dev/null +++ b/core/src/util.rs @@ -0,0 +1,30 @@ +use crate::sea_orm::DbConn; + +#[cfg(test)] +pub async fn get_memory_sqlite_connection() -> DbConn { + use entity::{admin, candidate, parent, session}; + use sea_orm::{Schema, Database}; + use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, DbBackend}; + + let base_url = "sqlite::memory:"; + let db: DbConn = Database::connect(base_url).await.unwrap(); + + let schema = Schema::new(DbBackend::Sqlite); + let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); + let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); + let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity); + let stmt4: 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 +} \ No newline at end of file