Skip to content

Commit

Permalink
Question CRUD
Browse files Browse the repository at this point in the history
  • Loading branch information
KavikaPalletenne committed Nov 12, 2024
1 parent 7a560e8 commit 3ed9515
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 86 deletions.
1 change: 1 addition & 0 deletions backend/server/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pub mod organisation;
pub mod rating;
pub mod role;
pub mod application;
pub mod question;
94 changes: 94 additions & 0 deletions backend/server/src/handler/question.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use axum::extract::{Json, Path, State};
use axum::response::IntoResponse;
use axum::http::StatusCode;
use serde_json::json;
use crate::models::app::AppState;
use crate::models::auth::{AuthUser, CampaignAdmin, QuestionAdmin};
use crate::models::error::ChaosError;
use crate::models::question::{NewQuestion, Question};
use crate::models::transaction::DBTransaction;

pub struct QuestionHandler;

impl QuestionHandler {
pub async fn create(
State(state): State<AppState>,
Path(campaign_id): Path<i64>,
_admin: CampaignAdmin,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewQuestion>,
) -> Result<impl IntoResponse, ChaosError> {
let id = Question::create(
campaign_id,
data.title,
data.description,
data.common,
data.roles,
data.required,
data.question_data,
state.snowflake_generator,
&mut transaction.tx,
).await?;

transaction.tx.commit().await?;

Ok((StatusCode::OK, Json(json!({"id": id}))))
}

pub async fn get_all_by_campaign_and_role(
Path((campaign_id, role_id)): Path<(i64, i64)>,
_user: AuthUser,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
let questions = Question::get_all_by_campaign_and_role(
campaign_id, role_id, &mut transaction.tx
).await?;

transaction.tx.commit().await?;

Ok((StatusCode::OK, Json(questions)))
}

pub async fn get_all_common_by_campaign(
Path(campaign_id): Path<i64>,
_user: AuthUser,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
let questions = Question::get_all_common_by_campaign(
campaign_id, &mut transaction.tx,
).await?;

transaction.tx.commit().await?;

Ok((StatusCode::OK, Json(questions)))
}

pub async fn update(
State(state): State<AppState>,
Path(question_id): Path<i64>,
_admin: QuestionAdmin,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewQuestion>,
) -> Result<impl IntoResponse, ChaosError> {
Question::update(
question_id, data.title, data.description, data.common,
data.roles, data.required, data.question_data,
&mut transaction.tx, state.snowflake_generator,
).await?;

Ok((StatusCode::OK, "Successfully updated question"))
}

pub async fn delete(
Path(question_id): Path<i64>,
_admin: QuestionAdmin,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
Question::delete(
question_id,
&mut transaction.tx,
).await?;

Ok((StatusCode::OK, "Successfully deleted question"))
}
}
9 changes: 7 additions & 2 deletions backend/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::handler::organisation::OrganisationHandler;
use crate::handler::application::ApplicationHandler;
use crate::models::storage::Storage;
use anyhow::Result;
use axum::routing::{get, patch, post};
use axum::routing::{get, patch, post, put};
use axum::Router;
use handler::rating::RatingHandler;
use handler::role::RoleHandler;
Expand All @@ -14,6 +14,7 @@ use models::app::AppState;
use snowflake::SnowflakeIdGenerator;
use sqlx::postgres::PgPoolOptions;
use std::env;
use crate::handler::question::QuestionHandler;

mod handler;
mod models;
Expand Down Expand Up @@ -116,7 +117,7 @@ async fn main() -> Result<()> {
)
.route(
"/api/v1/:application_id/rating",
post(RatingHandler::create_rating),
post(RatingHandler::create),
)
.route(
"/api/v1/:application_id/ratings",
Expand All @@ -126,6 +127,7 @@ async fn main() -> Result<()> {
"/api/v1/campaign/:campaign_id/role",
post(CampaignHandler::create_role),
)
.route("/api/v1/campaign/:campaign_id/role/:role_id/questions", get(QuestionHandler::get_all_by_campaign_and_role))
.route(
"/api/v1/campaign/:campaign_id/roles",
get(CampaignHandler::get_roles),
Expand All @@ -151,6 +153,9 @@ async fn main() -> Result<()> {
.delete(CampaignHandler::delete),
)
.route("/api/v1/campaign", get(CampaignHandler::get_all))
.route("/api/v1/campaign/:campaign_id/question", post(QuestionHandler::create))
.route("/api/v1/campaign/:campaign_id/question/:id", put(QuestionHandler::update).delete(QuestionHandler::delete))
.route("/api/v1/campaign/:campaign_id/questions/common", get(QuestionHandler::get_all_common_by_campaign))
.route(
"/api/v1/campaign/:campaign_id/banner",
patch(CampaignHandler::update_banner),
Expand Down
43 changes: 43 additions & 0 deletions backend/server/src/models/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use axum::response::{IntoResponse, Redirect, Response};
use axum::{async_trait, RequestPartsExt};
use axum_extra::{headers::Cookie, TypedHeader};
use serde::{Deserialize, Serialize};
use crate::service::question::user_is_question_admin;

// tells the web framework how to take the url query params they will have
#[derive(Deserialize, Serialize)]
Expand Down Expand Up @@ -455,3 +456,45 @@ where
Ok(RatingCreator { user_id })
}
}

pub struct QuestionAdmin {
pub user_id: i64,
}

#[async_trait]
impl<S> FromRequestParts<S> for QuestionAdmin
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 decoding_key = &app_state.decoding_key;
let jwt_validator = &app_state.jwt_validator;
let TypedHeader(cookies) = parts
.extract::<TypedHeader<Cookie>>()
.await
.map_err(|_| ChaosError::NotLoggedIn)?;

let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?;

let claims =
decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?;

let pool = &app_state.db;
let user_id = claims.sub;

let question_id = *parts
.extract::<Path<HashMap<String, i64>>>()
.await
.map_err(|_| ChaosError::BadRequest)?
.get("question_id")
.ok_or(ChaosError::BadRequest)?;

user_is_question_admin(user_id, question_id, pool).await?;

Ok(QuestionAdmin { user_id })
}
}
90 changes: 6 additions & 84 deletions backend/server/src/models/question.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ pub struct Question {

#[derive(Deserialize)]
pub struct NewQuestion {
title: String,
description: Option<String>,
common: bool,
roles: Option<Vec<i64>>,
required: bool,
pub title: String,
pub description: Option<String>,
pub common: bool,
pub roles: Option<Vec<i64>>,
pub required: bool,

#[serde(flatten)]
question_data: QuestionData,
pub question_data: QuestionData,
}

#[derive(Deserialize, Serialize, sqlx::FromRow)]
Expand Down Expand Up @@ -519,84 +519,6 @@ impl QuestionData {
}
}

pub async fn get_from_db(self, question_id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<Self, ChaosError> {
match self {
Self::ShortAnswer => Ok(Self::ShortAnswer),
Self::MultiChoice(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::MultiChoice(data))
},
Self::MultiSelect(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::MultiSelect(data))
}
Self::DropDown(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::DropDown(data))
}
Self::Ranking(_) => {
let data_vec = sqlx::query_as!(
MultiOptionQuestionOption,
"
SELECT id, display_order, text FROM multi_option_question_options
WHERE question_id = $1
",
question_id
)
.fetch_all(transaction.deref_mut())
.await?;

let data = MultiOptionData {
options: data_vec
};

Ok(Self::Ranking(data))
}
}
}

pub async fn delete_from_db(self, question_id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> {
match self {
Self::ShortAnswer => Ok(()),
Expand Down
1 change: 1 addition & 0 deletions backend/server/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod organisation;
pub mod rating;
pub mod role;
pub mod application;
pub mod question;
32 changes: 32 additions & 0 deletions backend/server/src/service/question.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use sqlx::{Pool, Postgres};
use crate::models::error::ChaosError;

pub async fn user_is_question_admin(
user_id: i64,
question_id: i64,
pool: &Pool<Postgres>,
) -> Result<(), ChaosError> {
let is_admin = sqlx::query!(
"
SELECT EXISTS(
SELECT 1
FROM questions q
JOIN campaigns c on q.campaign_id = c.id
JOIN organisation_members om on c.organisation_id = om.organisation_id
WHERE q.id = $1 AND om.user_id = $2 AND om.role = 'Admin'
)
",
question_id,
user_id
)
.fetch_one(pool)
.await?
.exists
.expect("`exists` should always exist in this query result");

if !is_admin {
return Err(ChaosError::Unauthorized);
}

Ok(())
}

0 comments on commit 3ed9515

Please sign in to comment.