Skip to content

Commit

Permalink
lock application after submission and campaign close date
Browse files Browse the repository at this point in the history
  • Loading branch information
KavikaPalletenne committed Dec 2, 2024
1 parent 299ee9b commit 43e47cd
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 47 deletions.
4 changes: 4 additions & 0 deletions backend/server/src/handler/answer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,6 +16,7 @@ impl AnswerHandler {
State(state): State<AppState>,
Path(application_id): Path<i64>,
_user: ApplicationOwner,
_: OpenApplicationByApplicationId,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewAnswer>,
) -> Result<impl IntoResponse, ChaosError> {
Expand Down Expand Up @@ -63,6 +65,7 @@ impl AnswerHandler {
pub async fn update(
Path(answer_id): Path<i64>,
_owner: AnswerOwner,
_: OpenApplicationByAnswerId,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewAnswer>,
) -> Result<impl IntoResponse, ChaosError> {
Expand All @@ -76,6 +79,7 @@ impl AnswerHandler {
pub async fn delete(
Path(answer_id): Path<i64>,
_owner: AnswerOwner,
_: OpenApplicationByAnswerId,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
Answer::delete(answer_id, &mut transaction.tx).await?;
Expand Down
3 changes: 2 additions & 1 deletion backend/server/src/handler/application.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -62,6 +62,7 @@ impl ApplicationHandler {

pub async fn submit(
_user: ApplicationOwner,
_: OpenApplicationByApplicationId,
Path(application_id): Path<i64>,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
Expand Down
36 changes: 1 addition & 35 deletions backend/server/src/models/answer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,6 @@ impl Answer {
) -> Result<i64, ChaosError> {
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!(
Expand Down Expand Up @@ -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?;

Expand All @@ -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(())
}
}
Expand Down
72 changes: 61 additions & 11 deletions backend/server/src/models/application.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use std::collections::HashMap;
use crate::models::error::ChaosError;
use crate::models::user::UserDetails;
use chrono::{DateTime, Utc};
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 {
Expand Down Expand Up @@ -417,14 +424,6 @@ impl Application {
roles: Vec<ApplicationRole>,
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
Expand Down Expand Up @@ -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
)
Expand All @@ -470,3 +467,56 @@ impl Application {
Ok(())
}
}


pub struct OpenApplicationByApplicationId;

#[async_trait]
impl<S> FromRequestParts<S> for OpenApplicationByApplicationId
where
AppState: FromRef<S>,
S: Send + Sync,
{
type Rejection = ChaosError;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);

let application_id = *parts
.extract::<Path<HashMap<String, i64>>>()
.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<S> FromRequestParts<S> for OpenApplicationByAnswerId
where
AppState: FromRef<S>,
S: Send + Sync,
{
type Rejection = ChaosError;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);

let answer_id = *parts
.extract::<Path<HashMap<String, i64>>>()
.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)
}
}
4 changes: 4 additions & 0 deletions backend/server/src/models/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub enum ChaosError {
#[error("Bad request")]
BadRequest,

#[error("Application closed")]
ApplicationClosed,

#[error("SQLx error")]
DatabaseError(#[from] sqlx::Error),

Expand Down Expand Up @@ -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(),
Expand Down
25 changes: 25 additions & 0 deletions backend/server/src/service/answer.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use chrono::Utc;
use crate::models::error::ChaosError;
use sqlx::{Pool, Postgres};

Expand Down Expand Up @@ -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<Postgres>,
) -> 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(())
}
24 changes: 24 additions & 0 deletions backend/server/src/service/application.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use chrono::Utc;
use crate::models::error::ChaosError;
use sqlx::{Pool, Postgres};

Expand Down Expand Up @@ -60,3 +61,26 @@ pub async fn user_is_application_owner(

Ok(())
}

pub async fn assert_application_is_open(
application_id: i64,
pool: &Pool<Postgres>,
) -> 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(())
}

0 comments on commit 43e47cd

Please sign in to comment.