Merge pull request #38 from EETagent/finish_file_upload

File upload & Candidate folder
This commit is contained in:
Vojtěch Jungmann 2022-11-14 23:30:46 +01:00 committed by GitHub
commit efac2e1965
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 322 additions and 105 deletions

157
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"] }

View file

@ -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() {

View file

@ -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
}
}
}

View file

@ -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,
}
}
}

View file

@ -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::*;

View file

@ -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<u8>) -> Result<(), ServiceError> {
// TODO
async fn write_portfolio_file(
candidate_id: i32,
data: Vec<u8>,
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<u8>) -> Result<(), ServiceError> {
Self::write_portfolio_file(candidate_id, letter, "MOTIVACNI_DOPIS.pdf").await
}
pub async fn add_portfolio_letter(
candidate_id: i32,
letter: Vec<u8>,
) -> 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<u8>) -> 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<Vec<u8>, 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<String, ServiceError> {
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;

View file

@ -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";

30
core/src/util.rs Normal file
View file

@ -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
}