From e360983d88d5c5873cfcd2c5276763319bbe41aa Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 5 Dec 2022 20:13:17 +0100 Subject: [PATCH 1/4] feat: add cache item delete service --- api/src/lib.rs | 37 ++++++---- api/src/routes/candidate.rs | 95 ++++++++++++++++++++------ core/src/services/portfolio_service.rs | 9 +++ 3 files changed, 105 insertions(+), 36 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index 860a118..b22f2a2 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,10 +1,10 @@ #[macro_use] extern crate rocket; -use rocket::fairing::{self, AdHoc, Fairing, Kind, Info}; +use rocket::fairing::{self, AdHoc, Fairing, Info, Kind}; use rocket::http::Header; -use rocket::{Build, Rocket, Request, Response}; +use rocket::{Build, Request, Response, Rocket}; use migration::MigratorTrait; use sea_orm_rocket::Database; @@ -27,14 +27,20 @@ impl Fairing for CORS { fn info(&self) -> Info { Info { name: "Add CORS headers to responses", - kind: Kind::Response + kind: Kind::Response, } } #[cfg(debug_assertions)] async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) { - response.set_header(Header::new("Access-Control-Allow-Origin", "http://localhost:5173")); - response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, OPTIONS")); + response.set_header(Header::new( + "Access-Control-Allow-Origin", + "http://localhost:5173", + )); + response.set_header(Header::new( + "Access-Control-Allow-Methods", + "POST, GET, OPTIONS", + )); response.set_header(Header::new("Access-Control-Allow-Headers", "content-type")); response.set_header(Header::new("Access-Control-Allow-Credentials", "true")); } @@ -61,7 +67,7 @@ async fn run_migrations(rocket: Rocket) -> fairing::Result { Ok(rocket) } -pub fn rocket() -> Rocket{ +pub fn rocket() -> Rocket { rocket::build() .attach(CORS) .attach(Db::init()) @@ -86,6 +92,14 @@ pub fn rocket() -> Rocket{ routes::candidate::upload_cover_letter, ], ) + .mount( + "/candidate/remove", + routes![ + routes::candidate::delete_portfolio_letter, + routes::candidate::delete_portfolio_zip, + routes::candidate::delete_cover_letter, + ], + ) .mount( "/candidate/portfolio", routes![ @@ -108,20 +122,13 @@ pub fn rocket() -> Rocket{ routes::admin::get_candidate_portfolio, ], ) - .mount( - "/admin/list", - routes![ - routes::admin::list_candidates, - ]) + .mount("/admin/list", routes![routes::admin::list_candidates,]) .register("/", catchers![]) } #[tokio::main] async fn start() -> Result<(), rocket::Error> { - rocket() - .launch() - .await - .map(|_| ()) + rocket().launch().await.map(|_| ()) } pub fn main() { diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index fa04a09..c2b3a6f 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -33,8 +33,8 @@ pub async fn login( login_form.password.to_string(), ip_addr.ip().to_string(), ) - .await - .map_err(to_custom_error)?; + .await + .map_err(to_custom_error)?; cookies.add_private(Cookie::new("id", session_token.clone())); cookies.add_private(Cookie::new("key", private_key.clone())); @@ -43,14 +43,22 @@ pub async fn login( } #[post("/logout")] -pub async fn logout(conn: Connection<'_, Db>, _session: CandidateAuth, cookies: &CookieJar<'_>,) -> Result<(), Custom> { +pub async fn logout( + conn: Connection<'_, Db>, + _session: CandidateAuth, + cookies: &CookieJar<'_>, +) -> Result<(), Custom> { let db = conn.into_inner(); - let cookie = cookies.get_private("id") // unwrap would be safe here because of the auth guard - .ok_or(Custom(Status::Unauthorized, "No session cookie".to_string()))?; + let cookie = cookies + .get_private("id") // unwrap would be safe here because of the auth guard + .ok_or(Custom( + Status::Unauthorized, + "No session cookie".to_string(), + ))?; 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()))?; - + CandidateService::logout(db, session_id) .await .map_err(to_custom_error)?; @@ -82,9 +90,7 @@ pub async fn post_details( .await .map_err(to_custom_error)?; - Ok( - Json(form) - ) + Ok(Json(form)) } #[get("/details")] @@ -118,18 +124,18 @@ pub async fn upload_cover_letter( Ok(()) } -#[get("/submission_progress")] -pub async fn submission_progress( - session: CandidateAuth -) -> Result, Custom> { +#[delete("/cover_letter")] +pub async fn delete_cover_letter(session: CandidateAuth) -> Result<(), Custom> { let candidate: entity::candidate::Model = session.into(); - let progress = PortfolioService::get_submission_progress(candidate.application) - .await - .map(|x| Json(x)) - .map_err(to_custom_error); + PortfolioService::delete_cache_item( + candidate.application, + portfolio_core::services::portfolio_service::FileType::CoverLetterPdf, + ) + .await + .map_err(to_custom_error)?; - progress + Ok(()) } #[post("/portfolio_letter", data = "")] @@ -146,6 +152,20 @@ pub async fn upload_portfolio_letter( Ok(()) } +#[delete("/portfolio_letter")] +pub async fn delete_portfolio_letter(session: CandidateAuth) -> Result<(), Custom> { + let candidate: entity::candidate::Model = session.into(); + + PortfolioService::delete_cache_item( + candidate.application, + portfolio_core::services::portfolio_service::FileType::PortfolioLetterPdf, + ) + .await + .map_err(to_custom_error)?; + + Ok(()) +} + #[post("/portfolio_zip", data = "")] pub async fn upload_portfolio_zip( session: CandidateAuth, @@ -160,6 +180,34 @@ pub async fn upload_portfolio_zip( Ok(()) } +#[delete("/portfolio_zip")] +pub async fn delete_portfolio_zip(session: CandidateAuth) -> Result<(), Custom> { + let candidate: entity::candidate::Model = session.into(); + + PortfolioService::delete_cache_item( + candidate.application, + portfolio_core::services::portfolio_service::FileType::PortfolioZip, + ) + .await + .map_err(to_custom_error)?; + + Ok(()) +} + +#[get("/submission_progress")] +pub async fn submission_progress( + session: CandidateAuth, +) -> Result, Custom> { + let candidate: entity::candidate::Model = session.into(); + + let progress = PortfolioService::get_submission_progress(candidate.application) + .await + .map(|x| Json(x)) + .map_err(to_custom_error); + + progress +} + #[post("/submit")] pub async fn submit_portfolio( conn: Connection<'_, Db>, @@ -177,7 +225,9 @@ pub async fn submit_portfolio( // TODO: VĂ­ce kontrol? if e.code() == 500 { // Cleanup - PortfolioService::delete_portfolio(candidate.application).await.unwrap(); + PortfolioService::delete_portfolio(candidate.application) + .await + .unwrap(); } return Err(to_custom_error(e)); } @@ -213,13 +263,16 @@ pub async fn download_portfolio(session: CandidateAuth) -> Result, Custo #[cfg(test)] mod tests { - use portfolio_core::{models::candidate::ApplicationDetails, crypto, sea_orm::prelude::Uuid}; + use portfolio_core::{crypto, models::candidate::ApplicationDetails, sea_orm::prelude::Uuid}; use rocket::{ http::{Cookie, Status}, local::blocking::Client, }; - use crate::{test::tests::{test_client, APPLICATION_ID, CANDIDATE_PASSWORD}, routes::admin::tests::admin_login}; + use crate::{ + routes::admin::tests::admin_login, + test::tests::{test_client, APPLICATION_ID, CANDIDATE_PASSWORD}, + }; fn candidate_login(client: &Client) -> (Cookie, Cookie) { let response = client diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index 89deb3b..e80463a 100644 --- a/core/src/services/portfolio_service.rs +++ b/core/src/services/portfolio_service.rs @@ -224,6 +224,15 @@ impl PortfolioService { true } + // Delete single item from cache + pub async fn delete_cache_item(candidate_id: i32, file_type: FileType) -> Result<(), ServiceError> { + let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); + + tokio::fs::remove_file(cache_path.join(file_type.as_str())).await?; + + Ok(()) + } + /// Removes all files from cache pub async fn delete_cache(candidate_id: i32) -> Result<(), ServiceError> { let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); From b31b557ae03e135244136f611a70542b52078b67 Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 5 Dec 2022 20:18:24 +0100 Subject: [PATCH 2/4] feat: use seperate functions --- api/src/routes/candidate.rs | 27 +++++++++----------------- core/src/services/portfolio_service.rs | 20 ++++++++++++++++++- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index c2b3a6f..dafe0b9 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -128,12 +128,9 @@ pub async fn upload_cover_letter( pub async fn delete_cover_letter(session: CandidateAuth) -> Result<(), Custom> { let candidate: entity::candidate::Model = session.into(); - PortfolioService::delete_cache_item( - candidate.application, - portfolio_core::services::portfolio_service::FileType::CoverLetterPdf, - ) - .await - .map_err(to_custom_error)?; + PortfolioService::delete_cover_letter_from_cache(candidate.application) + .await + .map_err(to_custom_error)?; Ok(()) } @@ -156,12 +153,9 @@ pub async fn upload_portfolio_letter( pub async fn delete_portfolio_letter(session: CandidateAuth) -> Result<(), Custom> { let candidate: entity::candidate::Model = session.into(); - PortfolioService::delete_cache_item( - candidate.application, - portfolio_core::services::portfolio_service::FileType::PortfolioLetterPdf, - ) - .await - .map_err(to_custom_error)?; + PortfolioService::delete_portfolio_letter_from_cache(candidate.application) + .await + .map_err(to_custom_error)?; Ok(()) } @@ -184,12 +178,9 @@ pub async fn upload_portfolio_zip( pub async fn delete_portfolio_zip(session: CandidateAuth) -> Result<(), Custom> { let candidate: entity::candidate::Model = session.into(); - PortfolioService::delete_cache_item( - candidate.application, - portfolio_core::services::portfolio_service::FileType::PortfolioZip, - ) - .await - .map_err(to_custom_error)?; + PortfolioService::delete_portfolio_zip_from_cache(candidate.application) + .await + .map_err(to_custom_error)?; Ok(()) } diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index e80463a..c122cc9 100644 --- a/core/src/services/portfolio_service.rs +++ b/core/src/services/portfolio_service.rs @@ -210,7 +210,7 @@ impl PortfolioService { /// Returns true if portfolio is ready to be moved to the final directory - pub async fn is_portfolio_prepared(candidate_id: i32) -> bool { + 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]; @@ -233,6 +233,24 @@ impl PortfolioService { Ok(()) } + pub async fn delete_cover_letter_from_cache( + candidate_id: i32, + ) -> Result<(), ServiceError> { + Self::delete_cache_item(candidate_id, FileType::CoverLetterPdf).await + } + + pub async fn delete_portfolio_letter_from_cache( + candidate_id: i32, + ) -> Result<(), ServiceError> { + Self::delete_cache_item(candidate_id, FileType::PortfolioLetterPdf).await + } + + pub async fn delete_portfolio_zip_from_cache( + candidate_id: i32, + ) -> Result<(), ServiceError> { + Self::delete_cache_item(candidate_id, FileType::PortfolioZip).await + } + /// Removes all files from cache pub async fn delete_cache(candidate_id: i32) -> Result<(), ServiceError> { let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); From 0465b4665094089953fbeab7578c26d31ea84654 Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 5 Dec 2022 20:25:04 +0100 Subject: [PATCH 3/4] feat: test cache delete functions --- core/src/services/portfolio_service.rs | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/core/src/services/portfolio_service.rs b/core/src/services/portfolio_service.rs index c122cc9..556f31c 100644 --- a/core/src/services/portfolio_service.rs +++ b/core/src/services/portfolio_service.rs @@ -430,6 +430,20 @@ mod tests { clear_data_store_temp_dir(temp_dir).await; } + #[tokio::test] + #[serial] + async fn test_delete_cover_letter_from_cache() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + PortfolioService::delete_cover_letter_from_cache(APPLICATION_ID).await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("MOTIVACNI_DOPIS.pdf")).await.is_err()); + + clear_data_store_temp_dir(temp_dir).await; + } + #[tokio::test] #[serial] async fn test_is_cover_letter() { @@ -442,6 +456,21 @@ mod tests { clear_data_store_temp_dir(temp_dir).await; } + #[tokio::test] + #[serial] + async fn test_delete_cache_item() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + PortfolioService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + PortfolioService::delete_cache_item(APPLICATION_ID, FileType::CoverLetterPdf).await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("MOTIVACNI_DOPIS.pdf")).await.is_err()); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] #[serial] async fn test_add_portfolio_letter_to_cache() { @@ -454,6 +483,20 @@ mod tests { clear_data_store_temp_dir(temp_dir).await; } + #[tokio::test] + #[serial] + async fn test_delete_portfolio_letter_from_cache() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + PortfolioService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + PortfolioService::delete_portfolio_letter_from_cache(APPLICATION_ID).await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("PORTFOLIO.pdf")).await.is_err()); + + clear_data_store_temp_dir(temp_dir).await; + } + #[tokio::test] #[serial] async fn test_is_portfolio_letter() { @@ -478,6 +521,20 @@ mod tests { clear_data_store_temp_dir(temp_dir).await; } + #[tokio::test] + #[serial] + async fn test_delete_portfolio_zip_from_cache() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + PortfolioService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + PortfolioService::delete_portfolio_zip_from_cache(APPLICATION_ID).await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("PORTFOLIO.zip")).await.is_err()); + + clear_data_store_temp_dir(temp_dir).await; + } + #[tokio::test] #[serial] async fn test_is_portfolio_zip() { From e376149c84b506e3cef8bf9f22aff6bee1c56322 Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 5 Dec 2022 20:32:20 +0100 Subject: [PATCH 4/4] feat: add api client to frontend --- frontend/src/lib/@api/candidate.ts | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/frontend/src/lib/@api/candidate.ts b/frontend/src/lib/@api/candidate.ts index eb1ae20..fcd82b9 100644 --- a/frontend/src/lib/@api/candidate.ts +++ b/frontend/src/lib/@api/candidate.ts @@ -111,6 +111,17 @@ export const apiUploadCoverLetter = async ( } }; +export const apiDeleteCoverLetter = async (): Promise => { + try { + await axios.delete(API_URL + '/candidate/remove/cover_letter', { + withCredentials: true, + }); + return true; + } catch (e) { + throw errorHandler(e, 'Failed to delete cover letter'); + } +}; + export const apiUploadPortfolioLetter = async ( letter: File, progressReporter: (progress: AxiosProgressEvent) => void @@ -130,6 +141,17 @@ export const apiUploadPortfolioLetter = async ( } }; +export const apiDeletePortfolioLetter = async (): Promise => { + try { + await axios.delete(API_URL + '/candidate/remove/portfolio_letter', { + withCredentials: true, + }); + return true; + } catch (e) { + throw errorHandler(e, 'Failed to delete portfolio letter'); + } +}; + export const apiUploadPortfolioZip = async ( portfolio: File, progressReporter: (progress: AxiosProgressEvent) => void @@ -149,6 +171,17 @@ export const apiUploadPortfolioZip = async ( } }; +export const apiDeletePortfolioZip = async (): Promise => { + try { + await axios.delete(API_URL + '/candidate/remove/portfolio_zip', { + withCredentials: true, + }); + return true; + } catch (e) { + throw errorHandler(e, 'Failed to delete portfolio zip'); + } +}; + export const apiSubmitPortfolio = async (): Promise => { try { await axios.post(API_URL + '/candidate/portfolio/submit', {}, { withCredentials: true });