From 43e47cd81e21d53d566d5afd13a2a0a576ef5df3 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 01:06:30 +1100 Subject: [PATCH] lock application after submission and campaign close date --- backend/server/src/handler/answer.rs | 4 ++ backend/server/src/handler/application.rs | 3 +- backend/server/src/models/answer.rs | 36 +----------- backend/server/src/models/application.rs | 72 +++++++++++++++++++---- backend/server/src/models/error.rs | 4 ++ backend/server/src/service/answer.rs | 25 ++++++++ backend/server/src/service/application.rs | 24 ++++++++ 7 files changed, 121 insertions(+), 47 deletions(-) diff --git a/backend/server/src/handler/answer.rs b/backend/server/src/handler/answer.rs index 065be1da..102e586b 100644 --- a/backend/server/src/handler/answer.rs +++ b/backend/server/src/handler/answer.rs @@ -7,6 +7,7 @@ use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; use serde_json::json; +use crate::models::application::{OpenApplicationByAnswerId, OpenApplicationByApplicationId}; pub struct AnswerHandler; @@ -15,6 +16,7 @@ impl AnswerHandler { State(state): State, Path(application_id): Path, _user: ApplicationOwner, + _: OpenApplicationByApplicationId, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { @@ -63,6 +65,7 @@ impl AnswerHandler { pub async fn update( Path(answer_id): Path, _owner: AnswerOwner, + _: OpenApplicationByAnswerId, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { @@ -76,6 +79,7 @@ impl AnswerHandler { pub async fn delete( Path(answer_id): Path, _owner: AnswerOwner, + _: OpenApplicationByAnswerId, mut transaction: DBTransaction<'_>, ) -> Result { Answer::delete(answer_id, &mut transaction.tx).await?; diff --git a/backend/server/src/handler/application.rs b/backend/server/src/handler/application.rs index 7c9cdca5..00afd7cf 100644 --- a/backend/server/src/handler/application.rs +++ b/backend/server/src/handler/application.rs @@ -1,5 +1,5 @@ use crate::models::app::AppState; -use crate::models::application::{Application, ApplicationRoleUpdate, ApplicationStatus}; +use crate::models::application::{Application, ApplicationRoleUpdate, ApplicationStatus, OpenApplicationByApplicationId}; use crate::models::auth::{ApplicationAdmin, ApplicationOwner, AuthUser}; use crate::models::error::ChaosError; use crate::models::transaction::DBTransaction; @@ -62,6 +62,7 @@ impl ApplicationHandler { pub async fn submit( _user: ApplicationOwner, + _: OpenApplicationByApplicationId, Path(application_id): Path, mut transaction: DBTransaction<'_>, ) -> Result { diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index 93ea3f0d..793ef0db 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -69,16 +69,6 @@ impl Answer { ) -> Result { answer_data.validate()?; - // Can only answer for applications that haven't been submitted - let _ = sqlx::query!( - " - SELECT id FROM applications WHERE id = $1 AND submitted = false - ", - application_id - ) - .fetch_one(transaction.deref_mut()) - .await?; - let id = snowflake_generator.generate(); sqlx::query!( @@ -309,16 +299,6 @@ impl Answer { .fetch_one(transaction.deref_mut()) .await?; - // Can only answer for applications that haven't been submitted - let _ = sqlx::query!( - " - SELECT id FROM applications WHERE id = $1 AND submitted = false - ", - answer.application_id - ) - .fetch_one(transaction.deref_mut()) - .await?; - let old_data = AnswerData::from_question_type(&answer.question_type); old_data.delete_from_db(id, transaction).await?; @@ -339,24 +319,10 @@ impl Answer { id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - let answer = sqlx::query!("SELECT application_id FROM answers WHERE id = $1", id) + let _ = sqlx::query!("DELETE FROM answers WHERE id = $1 RETURNING id", id) .fetch_one(transaction.deref_mut()) .await?; - // Can only answer for applications that haven't been submitted - let _ = sqlx::query!( - " - SELECT id FROM applications WHERE id = $1 AND submitted = false - ", - answer.application_id - ) - .fetch_one(transaction.deref_mut()) - .await?; - - sqlx::query!("DELETE FROM answers WHERE id = $1", id) - .execute(transaction.deref_mut()) - .await?; - Ok(()) } } diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index c6b403d0..66a3e94e 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use crate::models::error::ChaosError; use crate::models::user::UserDetails; use chrono::{DateTime, Utc}; @@ -5,6 +6,12 @@ use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{FromRow, Pool, Postgres, Transaction}; use std::ops::DerefMut; +use axum::{async_trait, RequestPartsExt}; +use axum::extract::{FromRef, FromRequestParts, Path}; +use axum::http::request::Parts; +use crate::models::app::AppState; +use crate::service::answer::assert_answer_application_is_open; +use crate::service::application::{assert_application_is_open}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Application { @@ -417,14 +424,6 @@ impl Application { roles: Vec, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - // Users can only update applications as long as they have not submitted - let _ = sqlx::query!( - "SELECT id FROM applications WHERE id = $1 AND submitted = false", - id - ) - .fetch_one(transaction.deref_mut()) - .await?; - sqlx::query!( " DELETE FROM application_roles WHERE application_id = $1 @@ -456,11 +455,9 @@ impl Application { id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - // Can only submit once let _ = sqlx::query!( " - UPDATE applications SET submitted = true - WHERE id = $1 AND submitted = false RETURNING id + UPDATE applications SET submitted = true WHERE id = $1 RETURNING id ", id ) @@ -470,3 +467,56 @@ impl Application { Ok(()) } } + + +pub struct OpenApplicationByApplicationId; + +#[async_trait] +impl FromRequestParts for OpenApplicationByApplicationId +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let app_state = AppState::from_ref(state); + + let application_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_application_is_open(application_id, &app_state.db).await?; + + Ok(OpenApplicationByApplicationId) + } +} + +pub struct OpenApplicationByAnswerId; + +#[async_trait] +impl FromRequestParts for OpenApplicationByAnswerId +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let app_state = AppState::from_ref(state); + + let answer_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_answer_application_is_open(answer_id, &app_state.db).await?; + + Ok(OpenApplicationByAnswerId) + } +} \ No newline at end of file diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 734ca8ba..58ca6b11 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -19,6 +19,9 @@ pub enum ChaosError { #[error("Bad request")] BadRequest, + #[error("Application closed")] + ApplicationClosed, + #[error("SQLx error")] DatabaseError(#[from] sqlx::Error), @@ -66,6 +69,7 @@ impl IntoResponse for ChaosError { (StatusCode::FORBIDDEN, "Forbidden operation").into_response() } ChaosError::BadRequest => (StatusCode::BAD_REQUEST, "Bad request").into_response(), + ChaosError::ApplicationClosed => (StatusCode::BAD_REQUEST, "Application closed").into_response(), ChaosError::DatabaseError(db_error) => match db_error { // We only care about the RowNotFound error, as others are miscellaneous DB errors. sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Not found").into_response(), diff --git a/backend/server/src/service/answer.rs b/backend/server/src/service/answer.rs index 682656a0..a0f8ca5b 100644 --- a/backend/server/src/service/answer.rs +++ b/backend/server/src/service/answer.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -30,3 +31,27 @@ pub async fn user_is_answer_owner( Ok(()) } + +pub async fn assert_answer_application_is_open( + answer_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let application = sqlx::query!( + " + SELECT app.submitted, c.ends_at FROM answers ans + JOIN applications app ON app.id = ans.application_id + JOIN campaigns c on c.id = app.campaign_id + WHERE ans.id = $1 + ", + answer_id + ) + .fetch_one(pool) + .await?; + + if application.submitted || application.ends_at <= time { + return Err(ChaosError::ApplicationClosed) + } + + Ok(()) +} \ No newline at end of file diff --git a/backend/server/src/service/application.rs b/backend/server/src/service/application.rs index dcdf755b..9198f167 100644 --- a/backend/server/src/service/application.rs +++ b/backend/server/src/service/application.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -60,3 +61,26 @@ pub async fn user_is_application_owner( Ok(()) } + +pub async fn assert_application_is_open( + application_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let application = sqlx::query!( + " + SELECT submitted, c.ends_at FROM applications a + JOIN campaigns c on c.id = a.campaign_id + WHERE a.id = $1 + ", + application_id + ) + .fetch_one(pool) + .await?; + + if application.submitted || application.ends_at <= time { + return Err(ChaosError::ApplicationClosed) + } + + Ok(()) +}