From 5f2c0cb7229c2c83f1c6c271499ed28fafee3d4b Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 26 Nov 2024 22:05:35 +1100 Subject: [PATCH 1/9] change all db pool in `Campaign` to transactions --- backend/server/src/handler/campaign.rs | 34 ++++++++++++++++---------- backend/server/src/models/campaign.rs | 23 ++++++++--------- backend/server/src/models/role.rs | 11 +++++---- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index ed5b9e24..75691ba4 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -15,67 +15,75 @@ use axum::response::IntoResponse; pub struct CampaignHandler; impl CampaignHandler { pub async fn get( - State(state): State, + mut transaction: DBTransaction<'_>, Path(id): Path, _user: AuthUser, ) -> Result { - let campaign = Campaign::get(id, &state.db).await?; + let campaign = Campaign::get(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, Json(campaign))) } pub async fn get_all( - State(state): State, + mut transaction: DBTransaction<'_>, _user: AuthUser, ) -> Result { - let campaigns = Campaign::get_all(&state.db).await?; + let campaigns = Campaign::get_all(&mut transaction.tx).await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, Json(campaigns))) } pub async fn update( - State(state): State, + mut transaction: DBTransaction<'_>, Path(id): Path, _admin: CampaignAdmin, Json(request_body): Json, ) -> Result { - Campaign::update(id, request_body, &state.db).await?; + Campaign::update(id, request_body, &mut transaction.tx).await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully updated campaign")) } pub async fn update_banner( + mut transaction: DBTransaction<'_>, State(state): State, Path(id): Path, _admin: CampaignAdmin, ) -> Result { - let banner_url = Campaign::update_banner(id, &state.db, &state.storage_bucket).await?; + let banner_url = Campaign::update_banner(id, &mut transaction.tx, &state.storage_bucket).await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, Json(banner_url))) } pub async fn delete( - State(state): State, + mut transaction: DBTransaction<'_>, Path(id): Path, _admin: CampaignAdmin, ) -> Result { - Campaign::delete(id, &state.db).await?; + Campaign::delete(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully deleted campaign")) } pub async fn create_role( + mut transaction: DBTransaction<'_>, State(state): State, Path(id): Path, _admin: CampaignAdmin, Json(data): Json, ) -> Result { - Role::create(id, data, &state.db, state.snowflake_generator).await?; + Role::create(id, data, &mut transaction.tx, state.snowflake_generator).await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully created role")) } pub async fn get_roles( - State(state): State, + mut transaction: DBTransaction<'_>, Path(id): Path, _user: AuthUser, ) -> Result { - let roles = Role::get_all_in_campaign(id, &state.db).await?; - + let roles = Role::get_all_in_campaign(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, Json(roles))) } diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index eb750de7..fa5881c0 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -1,7 +1,8 @@ +use std::ops::DerefMut; use chrono::{DateTime, Utc}; use s3::Bucket; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; +use sqlx::{FromRow, Transaction}; use sqlx::{Pool, Postgres}; use uuid::Uuid; @@ -57,7 +58,7 @@ pub struct CampaignBannerUpdate { impl Campaign { /// Get a list of all campaigns, both published and unpublished - pub async fn get_all(pool: &Pool) -> Result, ChaosError> { + pub async fn get_all(transaction: &mut Transaction<'_, Postgres>) -> Result, ChaosError> { let campaigns = sqlx::query_as!( Campaign, " @@ -65,14 +66,14 @@ impl Campaign { JOIN organisations o on c.organisation_id = o.id " ) - .fetch_all(pool) + .fetch_all(transaction.deref_mut()) .await?; Ok(campaigns) } /// Get a campaign based on it's id - pub async fn get(id: i64, pool: &Pool) -> Result { + pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { let campaign = sqlx::query_as!( CampaignDetails, " @@ -84,7 +85,7 @@ impl Campaign { ", id ) - .fetch_one(pool) + .fetch_one(transaction.deref_mut()) .await?; Ok(campaign) @@ -94,7 +95,7 @@ impl Campaign { pub async fn update( id: i64, update: CampaignUpdate, - pool: &Pool, + transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { _ = sqlx::query!( " @@ -108,7 +109,7 @@ impl Campaign { update.ends_at, id ) - .fetch_one(pool) + .fetch_one(transaction.deref_mut()) .await?; Ok(()) @@ -118,7 +119,7 @@ impl Campaign { /// Returns the updated campaign pub async fn update_banner( id: i64, - pool: &Pool, + transaction: &mut Transaction<'_, Postgres>, storage_bucket: &Bucket, ) -> Result { let dt = Utc::now(); @@ -135,7 +136,7 @@ impl Campaign { current_time, id ) - .fetch_one(pool) + .fetch_one(transaction.deref_mut()) .await?; let upload_url = @@ -145,14 +146,14 @@ impl Campaign { } /// Delete a campaign from the database - pub async fn delete(id: i64, pool: &Pool) -> Result<(), ChaosError> { + pub async fn delete(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { _ = sqlx::query!( " DELETE FROM campaigns WHERE id = $1 RETURNING id ", id ) - .fetch_one(pool) + .fetch_one(transaction.deref_mut()) .await?; Ok(()) diff --git a/backend/server/src/models/role.rs b/backend/server/src/models/role.rs index ff53014b..43013c93 100644 --- a/backend/server/src/models/role.rs +++ b/backend/server/src/models/role.rs @@ -1,8 +1,9 @@ +use std::ops::DerefMut; use crate::models::error::ChaosError; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; -use sqlx::{FromRow, Pool, Postgres}; +use sqlx::{FromRow, Pool, Postgres, Transaction}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Role { @@ -41,7 +42,7 @@ impl Role { pub async fn create( campaign_id: i64, role_data: RoleUpdate, - pool: &Pool, + transaction: &mut Transaction<'_, Postgres>, mut snowflake_generator: SnowflakeIdGenerator, ) -> Result<(), ChaosError> { let id = snowflake_generator.generate(); @@ -59,7 +60,7 @@ impl Role { role_data.max_avaliable, role_data.finalised ) - .execute(pool) + .execute(transaction.deref_mut()) .await?; Ok(()) @@ -123,7 +124,7 @@ impl Role { */ pub async fn get_all_in_campaign( campaign_id: i64, - pool: &Pool, + transaction: &mut Transaction<'_, Postgres>, ) -> Result, ChaosError> { let roles = sqlx::query_as!( RoleDetails, @@ -134,7 +135,7 @@ impl Role { ", campaign_id ) - .fetch_all(pool) + .fetch_all(transaction.deref_mut()) .await?; Ok(roles) From 1939b6b18c224b7e20b93f7c9fde8219ac516933 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 26 Nov 2024 22:28:00 +1100 Subject: [PATCH 2/9] move extracting user_id from request to helper function --- backend/server/src/models/auth.rs | 261 +++++++---------------------- backend/server/src/service/auth.rs | 37 +++- 2 files changed, 90 insertions(+), 208 deletions(-) diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index f28ac9f0..55dfb051 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -2,7 +2,7 @@ use crate::models::app::AppState; use crate::models::error::ChaosError; use crate::service::answer::user_is_answer_owner; use crate::service::application::{user_is_application_admin, user_is_application_owner}; -use crate::service::auth::is_super_user; +use crate::service::auth::{assert_is_super_user, extract_user_id_from_request}; use crate::service::campaign::user_is_campaign_admin; use crate::service::jwt::decode_auth_token; use crate::service::organisation::assert_user_is_organisation_admin; @@ -19,6 +19,7 @@ use axum::{async_trait, RequestPartsExt}; use axum_extra::{headers::Cookie, TypedHeader}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::service::email_template::user_is_email_template_admin; // tells the web framework how to take the url query params they will have #[derive(Deserialize, Serialize)] @@ -57,21 +58,10 @@ where type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; Ok(AuthUser { - user_id: claims.sub, + user_id, }) } } @@ -91,30 +81,13 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() - .await - .map_err(|_| ChaosError::NotLoggedIn)?; + let user_id = extract_user_id_from_request(parts, app_state).await?; - let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + assert_is_super_user(user_id, &app_state.db).await?; - let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; - - let pool = &app_state.db; - let possible_user = is_super_user(claims.sub, pool).await; - - if let Ok(is_auth_user) = possible_user { - if is_auth_user { - return Ok(SuperUser { - user_id: claims.sub, - }); - } - } - - Err(ChaosError::Unauthorized) + Ok(SuperUser { + user_id, + }) } } @@ -132,20 +105,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let organisation_id = *parts .extract::>>() @@ -154,7 +114,7 @@ where .get("organisation_id") .ok_or(ChaosError::BadRequest)?; - assert_user_is_organisation_admin(user_id, organisation_id, pool).await?; + assert_user_is_organisation_admin(user_id, organisation_id, &app_state.db).await?; Ok(OrganisationAdmin { user_id }) } @@ -174,20 +134,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let campaign_id = *parts .extract::>>() @@ -196,7 +143,7 @@ where .get("campaign_id") .ok_or(ChaosError::BadRequest)?; - user_is_campaign_admin(user_id, campaign_id, pool).await?; + user_is_campaign_admin(user_id, campaign_id, &app_state.db).await?; Ok(CampaignAdmin { user_id }) } @@ -216,20 +163,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let role_id = *parts .extract::>>() @@ -238,7 +172,7 @@ where .get("role_id") .ok_or(ChaosError::BadRequest)?; - user_is_role_admin(user_id, role_id, pool).await?; + user_is_role_admin(user_id, role_id, &app_state.db).await?; Ok(RoleAdmin { user_id }) } @@ -258,27 +192,14 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let Path(application_id) = parts .extract::>() .await .map_err(|_| ChaosError::BadRequest)?; - user_is_application_admin(user_id, application_id, pool).await?; + user_is_application_admin(user_id, application_id, &app_state.db).await?; Ok(ApplicationAdmin { user_id }) } @@ -305,29 +226,15 @@ where type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let Path(application_id) = parts .extract::>() .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_organisation_member(user_id, application_id, pool).await?; + assert_user_is_organisation_member(user_id, application_id, &app_state.db).await?; Ok(ApplicationReviewerGivenApplicationId { user_id }) } @@ -347,29 +254,15 @@ where type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let Path(application_id) = parts .extract::>() .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_organisation_member(user_id, application_id, pool).await?; + assert_user_is_organisation_member(user_id, application_id, &app_state.db).await?; Ok(ApplicationCreatorGivenApplicationId { user_id }) } @@ -389,29 +282,15 @@ where type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let Path(rating_id) = parts .extract::>() .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_application_reviewer_given_rating_id(user_id, rating_id, pool).await?; + assert_user_is_application_reviewer_given_rating_id(user_id, rating_id, &app_state.db).await?; Ok(ApplicationReviewerGivenRatingId { user_id }) } @@ -430,29 +309,15 @@ where type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - // TODO: put into separate function, since this is just getting the id through jwt, and duplicated here. 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let Path(rating_id) = parts .extract::>() .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_rating_creator_and_organisation_member(user_id, rating_id, pool).await?; + assert_user_is_rating_creator_and_organisation_member(user_id, rating_id, &app_state.db).await?; Ok(RatingCreator { user_id }) } @@ -472,20 +337,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() - .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 user_id= extract_user_id_from_request(parts, app_state).await?; let question_id = *parts .extract::>>() @@ -494,7 +346,7 @@ where .get("question_id") .ok_or(ChaosError::BadRequest)?; - user_is_question_admin(user_id, question_id, pool).await?; + user_is_question_admin(user_id, question_id, &app_state.db).await?; Ok(QuestionAdmin { user_id }) } @@ -514,20 +366,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() - .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 user_id = extract_user_id_from_request(parts, app_state).await?; let application_id = *parts .extract::>>() @@ -536,7 +375,7 @@ where .get("application_id") .ok_or(ChaosError::BadRequest)?; - user_is_application_owner(user_id, application_id, pool).await?; + user_is_application_owner(user_id, application_id, &app_state.db).await?; Ok(ApplicationOwner { user_id }) } @@ -556,30 +395,46 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 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::>() + let user_id = extract_user_id_from_request(parts, app_state).await?; + + let application_id = *parts + .extract::>>() .await - .map_err(|_| ChaosError::NotLoggedIn)?; + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; - let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + user_is_answer_owner(user_id, application_id, &app_state.db).await?; - let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; + Ok(AnswerOwner { user_id }) + } +} - let pool = &app_state.db; - let user_id = claims.sub; +pub struct EmailTemplateAdmin { + pub user_id: i64, +} - let application_id = *parts +#[async_trait] +impl FromRequestParts for EmailTemplateAdmin +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 user_id= extract_user_id_from_request(parts, app_state).await?; + + let template_id = *parts .extract::>>() .await .map_err(|_| ChaosError::BadRequest)? .get("application_id") .ok_or(ChaosError::BadRequest)?; - user_is_answer_owner(user_id, application_id, pool).await?; + user_is_email_template_admin(user_id, template_id, &app_state.db).await?; - Ok(AnswerOwner { user_id }) + Ok(EmailTemplateAdmin { user_id }) } } diff --git a/backend/server/src/service/auth.rs b/backend/server/src/service/auth.rs index cf136cfd..d0de71ed 100644 --- a/backend/server/src/service/auth.rs +++ b/backend/server/src/service/auth.rs @@ -1,7 +1,14 @@ +use axum::extract::FromRef; +use axum::http::request::Parts; +use axum::RequestPartsExt; +use axum_extra::headers::Cookie; +use axum_extra::TypedHeader; use crate::models::user::UserRole; -use anyhow::Result; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; +use crate::models::app::AppState; +use crate::models::error::ChaosError; +use crate::service::jwt::decode_auth_token; /// Checks if a user exists in DB based on given email address. If so, their user_id is returned. /// Otherwise, a new user is created in the DB, and the new id is returned. @@ -13,7 +20,7 @@ pub async fn create_or_get_user_id( name: String, pool: Pool, mut snowflake_generator: SnowflakeIdGenerator, -) -> Result { +) -> Result { let possible_user_id = sqlx::query!( "SELECT id FROM users WHERE lower(email) = $1", email.to_lowercase() @@ -39,14 +46,34 @@ pub async fn create_or_get_user_id( Ok(user_id) } -pub async fn is_super_user(user_id: i64, pool: &Pool) -> Result { +pub async fn assert_is_super_user(user_id: i64, pool: &Pool) -> Result<(), ChaosError> { let is_super_user = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1 AND role = $2)", user_id, UserRole::SuperUser as UserRole ) .fetch_one(pool) - .await?; + .await?.exists.expect("`exists` should always exist in this query result"); + + if !is_super_user { + return Err(ChaosError::Unauthorized); + } - Ok(is_super_user.exists.unwrap()) + Ok(()) } + +pub async fn extract_user_id_from_request(parts: &mut Parts, state: AppState) -> Result where S: Send + Sync { + let decoding_key = &state.decoding_key; + let jwt_validator = &state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .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)?; + + Ok(claims.sub) +} \ No newline at end of file From 62c6c9c07c372b28b7dcfa879af624f73e64dee9 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 26 Nov 2024 22:48:15 +1100 Subject: [PATCH 3/9] add slugs to `Campaign` and `Organisation` --- .../20240406024211_create_organisations.sql | 1 + .../20240406025537_create_campaigns.sql | 4 +- backend/server/src/handler/campaign.rs | 10 +++++ backend/server/src/models/campaign.rs | 35 +++++++++++++++-- backend/server/src/models/organisation.rs | 38 +++++++++++++++---- 5 files changed, 76 insertions(+), 12 deletions(-) diff --git a/backend/migrations/20240406024211_create_organisations.sql b/backend/migrations/20240406024211_create_organisations.sql index ec67f190..2c3bfdf7 100644 --- a/backend/migrations/20240406024211_create_organisations.sql +++ b/backend/migrations/20240406024211_create_organisations.sql @@ -1,5 +1,6 @@ CREATE TABLE organisations ( id BIGINT PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE, logo UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/backend/migrations/20240406025537_create_campaigns.sql b/backend/migrations/20240406025537_create_campaigns.sql index c300ee80..b1d8b6ec 100644 --- a/backend/migrations/20240406025537_create_campaigns.sql +++ b/backend/migrations/20240406025537_create_campaigns.sql @@ -1,6 +1,7 @@ CREATE TABLE campaigns ( id BIGINT PRIMARY KEY, organisation_id BIGINT NOT NULL, + slug TEXT NOT NULL, name TEXT NOT NULL, cover_image UUID, description TEXT, @@ -12,7 +13,8 @@ CREATE TABLE campaigns ( FOREIGN KEY(organisation_id) REFERENCES organisations(id) ON DELETE CASCADE - ON UPDATE CASCADE + ON UPDATE CASCADE, + UNIQUE (organisation_id, slug) ); CREATE TABLE campaign_roles ( diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index 75691ba4..a10bf0d8 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -24,6 +24,16 @@ impl CampaignHandler { Ok((StatusCode::OK, Json(campaign))) } + pub async fn get_by_slugs( + mut transaction: DBTransaction<'_>, + Path((organisation_slug, campaign_slug)): Path<(String, String)>, + _user: AuthUser, + ) -> Result { + let campaign = Campaign::get_by_slugs(organisation_slug, campaign_slug, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, Json(campaign))) + } + pub async fn get_all( mut transaction: DBTransaction<'_>, _user: AuthUser, diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index fa5881c0..47e361b6 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -11,6 +11,7 @@ use super::{error::ChaosError, storage::Storage}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Campaign { pub id: i64, + pub slug: String, pub name: String, pub organisation_id: i64, pub organisation_name: String, @@ -25,8 +26,10 @@ pub struct Campaign { #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct CampaignDetails { pub id: i64, + pub campaign_slug: String, pub name: String, pub organisation_id: i64, + pub organisation_slug: String, pub organisation_name: String, pub cover_image: Option, pub description: Option, @@ -36,6 +39,7 @@ pub struct CampaignDetails { #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct OrganisationCampaign { pub id: i64, + pub slug: String, pub name: String, pub cover_image: Option, pub description: Option, @@ -45,6 +49,7 @@ pub struct OrganisationCampaign { #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct CampaignUpdate { + pub slug: String, pub name: String, pub description: String, pub starts_at: DateTime, @@ -77,8 +82,9 @@ impl Campaign { let campaign = sqlx::query_as!( CampaignDetails, " - SELECT c.id, c.name, c.organisation_id, o.name as organisation_name, - c.cover_image, c.description, c.starts_at, c.ends_at + SELECT c.id, c.slug AS campaign_slug, c.name, c.organisation_id, + o.slug AS organisation_slug, o.name as organisation_name, c.cover_image, + c.description, c.starts_at, c.ends_at FROM campaigns c JOIN organisations o on c.organisation_id = o.id WHERE c.id = $1 @@ -91,6 +97,26 @@ impl Campaign { Ok(campaign) } + pub async fn get_by_slugs(organisation_slug: String, campaign_slug: String, transaction: &mut Transaction<'_, Postgres>) -> Result { + let campaign = sqlx::query_as!( + CampaignDetails, + " + SELECT c.id, c.slug AS campaign_slug, c.name, c.organisation_id, + o.slug AS organisation_slug, o.name as organisation_name, c.cover_image, + c.description, c.starts_at, c.ends_at + FROM campaigns c + JOIN organisations o on c.organisation_id = o.id + WHERE c.slug = $1 AND o.slug = $2 + ", + campaign_slug, + organisation_slug + ) + .fetch_one(transaction.deref_mut()) + .await?; + + Ok(campaign) + } + /// Update a campaign for all fields that are not None pub async fn update( id: i64, @@ -100,9 +126,10 @@ impl Campaign { _ = sqlx::query!( " UPDATE campaigns - SET name = $1, description = $2, starts_at = $3, ends_at = $4 - WHERE id = $5 RETURNING id + SET slug = $1, name = $2, description = $3, starts_at = $4, ends_at = $5 + WHERE id = $6 RETURNING id ", + update.slug, update.name, update.description, update.starts_at, diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 94a34c24..ce5f7634 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -12,6 +12,7 @@ use uuid::Uuid; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Organisation { pub id: i64, + pub slug: String, pub name: String, pub logo: Option, pub created_at: DateTime, @@ -22,6 +23,7 @@ pub struct Organisation { #[derive(Deserialize, Serialize)] pub struct NewOrganisation { + pub slug: String, pub name: String, pub admin: i64, } @@ -29,6 +31,7 @@ pub struct NewOrganisation { #[derive(Deserialize, Serialize)] pub struct OrganisationDetails { pub id: i64, + pub slug: String, pub name: String, pub logo: Option, pub created_at: DateTime, @@ -66,6 +69,7 @@ pub struct AdminToRemove { impl Organisation { pub async fn create( admin_id: i64, + slug: String, name: String, mut snowflake_generator: SnowflakeIdGenerator, transaction: &mut Transaction<'_, Postgres>, @@ -74,10 +78,11 @@ impl Organisation { sqlx::query!( " - INSERT INTO organisations (id, name) - VALUES ($1, $2) + INSERT INTO organisations (id, slug, name) + VALUES ($1, $2, $3) ", id, + slug, name ) .execute(transaction.deref_mut()) @@ -102,7 +107,7 @@ impl Organisation { let organisation = sqlx::query_as!( OrganisationDetails, " - SELECT id, name, logo, created_at + SELECT id, slug, name, logo, created_at FROM organisations WHERE id = $1 ", @@ -330,7 +335,7 @@ impl Organisation { let campaigns = sqlx::query_as!( OrganisationCampaign, " - SELECT id, name, cover_image, description, starts_at, ends_at + SELECT id, slug, name, cover_image, description, starts_at, ends_at FROM campaigns WHERE organisation_id = $1 ", @@ -344,22 +349,24 @@ impl Organisation { pub async fn create_campaign( organisation_id: i64, + slug: String, name: String, description: Option, starts_at: DateTime, ends_at: DateTime, pool: &Pool, - snowflake_id_generator: &mut SnowflakeIdGenerator, + mut snowflake_id_generator: SnowflakeIdGenerator, ) -> Result<(), ChaosError> { let new_campaign_id = snowflake_id_generator.real_time_generate(); sqlx::query!( " - INSERT INTO campaigns (id, organisation_id, name, description, starts_at, ends_at) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO campaigns (id, organisation_id, slug, name, description, starts_at, ends_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) ", new_campaign_id, organisation_id, + slug, name, description, starts_at, @@ -370,4 +377,21 @@ impl Organisation { Ok(()) } + + pub async fn create_email_template(organisation_id: i64, name: String, template: String, pool: &Pool, mut snowflake_generator: SnowflakeIdGenerator,) -> Result { + let id = snowflake_generator.generate(); + + let _ = sqlx::query!( + " + INSERT INTO email_templates (id, organisation_id, name, template) + VALUES ($1, $2, $3, $4) + ", + id, organisation_id, + name, template + ) + .execute(pool) + .await?; + + Ok(id) + } } From a13bb49037ad6c7416715bf3c55ff6a3bc12955d Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 26 Nov 2024 23:02:10 +1100 Subject: [PATCH 4/9] add endpoints to check slug availability --- backend/server/src/handler/campaign.rs | 5 +++-- backend/server/src/models/app.rs | 25 +++++++++++++++++++++++ backend/server/src/models/campaign.rs | 20 ++++++++++++++++++ backend/server/src/models/organisation.rs | 24 ++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index a10bf0d8..c76ecb87 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -2,15 +2,16 @@ use crate::models; use crate::models::app::AppState; use crate::models::application::Application; use crate::models::application::NewApplication; -use crate::models::auth::AuthUser; +use crate::models::auth::{AuthUser, SuperUser}; use crate::models::auth::CampaignAdmin; -use crate::models::campaign::Campaign; +use crate::models::campaign::{Campaign, CampaignSlugCheck}; use crate::models::error::ChaosError; use crate::models::role::{Role, RoleUpdate}; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; +use crate::models::organisation::{Organisation, SlugCheck}; pub struct CampaignHandler; impl CampaignHandler { diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index ed41b78f..290d7631 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -18,6 +18,8 @@ use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use sqlx::{Pool, Postgres}; use std::env; +use crate::handler::email_template::EmailTemplateHandler; +use crate::models::organisation::Organisation; #[derive(Clone)] pub struct AppState { @@ -89,6 +91,7 @@ pub async fn app() -> Result { get(ApplicationHandler::get_from_curr_user), ) .route("/api/v1/organisation", post(OrganisationHandler::create)) + .route("/api/v1/organisation/slug_check", post(OrganisationHandler::check_organisation_slug_availability)) .route( "/api/v1/organisation/:organisation_id", get(OrganisationHandler::get).delete(OrganisationHandler::delete), @@ -97,10 +100,22 @@ pub async fn app() -> Result { "/api/v1/organisation/:organisation_id/campaign", post(OrganisationHandler::create_campaign), ) + .route( + "/api/v1/organisation/:organisation_id/campaign/slug_check", + post(OrganisationHandler::check_campaign_slug_availability) + ) .route( "/api/v1/organisation/:organisation_id/campaigns", get(OrganisationHandler::get_campaigns), ) + .route( + "/api/v1/organisation/:organisation_id/email_template", + post(OrganisationHandler::create_email_template) + ) + .route( + "/api/v1/organisation/:organisation_id/email_templates", + get(OrganisationHandler::get_all_email_templates) + ) .route( "/api/v1/organisation/:organisation_id/logo", patch(OrganisationHandler::update_logo), @@ -163,6 +178,10 @@ pub async fn app() -> Result { .put(CampaignHandler::update) .delete(CampaignHandler::delete), ) + .route( + "/api/v1/campaign/slug/:organisation_slug/:campaign_slug", + get(CampaignHandler::get_by_slugs) + ) .route("/api/v1/campaign", get(CampaignHandler::get_all)) .route( "/api/v1/campaign/:campaign_id/question", @@ -212,5 +231,11 @@ pub async fn app() -> Result { "/api/v1/answer/:answer_id", patch(AnswerHandler::update).delete(AnswerHandler::delete), ) + .route( + "/api/v1/email_template/:template_id", + get(EmailTemplateHandler::get) + .patch(EmailTemplateHandler::update) + .delete(EmailTemplateHandler::delete) + ) .with_state(state)) } diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index 47e361b6..5295e54e 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -97,6 +97,26 @@ impl Campaign { Ok(campaign) } + pub async fn check_slug_availability(organisation_id: i64, slug: String, pool: &Pool) -> Result<(), ChaosError> { + let exists = sqlx::query!( + " + SELECT EXISTS(SELECT 1 FROM campaigns WHERE organisation_id = $1 AND slug = $2) + ", + organisation_id, + slug + ) + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); + + if exists { + return Err(ChaosError::BadRequest) + } + + Ok(()) + } + pub async fn get_by_slugs(organisation_slug: String, campaign_slug: String, transaction: &mut Transaction<'_, Postgres>) -> Result { let campaign = sqlx::query_as!( CampaignDetails, diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index ce5f7634..14ce224d 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -66,6 +66,11 @@ pub struct AdminToRemove { pub user_id: i64, } +#[derive(Deserialize)] +pub struct SlugCheck { + pub slug: String +} + impl Organisation { pub async fn create( admin_id: i64, @@ -103,6 +108,25 @@ impl Organisation { Ok(()) } + pub async fn check_slug_availability(slug: String, pool: &Pool) -> Result<(), ChaosError> { + let exists = sqlx::query!( + " + SELECT EXISTS(SELECT 1 FROM organisations WHERE slug = $1) + ", + slug + ) + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); + + if exists { + return Err(ChaosError::BadRequest) + } + + Ok(()) + } + pub async fn get(id: i64, pool: &Pool) -> Result { let organisation = sqlx::query_as!( OrganisationDetails, From 2ec7f6b749bbd054933c72d5a5033e039e9e4f5c Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 26 Nov 2024 23:25:37 +1100 Subject: [PATCH 5/9] slug utility functions and checks --- backend/server/src/handler/organisation.rs | 66 +++++++++++++++++++++- backend/server/src/models/app.rs | 2 + backend/server/src/models/campaign.rs | 4 ++ backend/server/src/models/organisation.rs | 28 +++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index f98b27b2..423b4713 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -3,11 +3,13 @@ use crate::models::app::AppState; use crate::models::auth::SuperUser; use crate::models::auth::{AuthUser, OrganisationAdmin}; use crate::models::error::ChaosError; -use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation}; +use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck, CampaignSlugCheck}; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; +use crate::models::campaign::{Campaign}; +use crate::models::email_template::EmailTemplate; pub struct OrganisationHandler; @@ -30,6 +32,16 @@ impl OrganisationHandler { Ok((StatusCode::OK, "Successfully created organisation")) } + pub async fn check_organisation_slug_availability( + State(state): State, + _user: SuperUser, + Json(data): Json, + ) -> Result { + Organisation::check_slug_availability(data.slug, &state.db).await?; + + Ok((StatusCode::OK, "Organisation slug is available")) + } + pub async fn get( State(state): State, Path(id): Path, @@ -39,6 +51,15 @@ impl OrganisationHandler { Ok((StatusCode::OK, Json(org))) } + pub async fn get_by_slug( + State(state): State, + Path(slug): Path, + _user: AuthUser, + ) -> Result { + let org = Organisation::get_by_slug(slug, &state.db).await?; + Ok((StatusCode::OK, Json(org))) + } + pub async fn delete( State(state): State, Path(id): Path, @@ -143,19 +164,58 @@ impl OrganisationHandler { Path(id): Path, State(mut state): State, _admin: OrganisationAdmin, - Json(request_body): Json, + Json(request_body): Json, ) -> Result { Organisation::create_campaign( id, + request_body.slug, request_body.name, request_body.description, request_body.starts_at, request_body.ends_at, &state.db, - &mut state.snowflake_generator, + state.snowflake_generator, ) .await?; Ok((StatusCode::OK, "Successfully created campaign")) } + + pub async fn check_campaign_slug_availability( + Path(organisation_id): Path, + State(state): State, + _user: OrganisationAdmin, + Json(data): Json, + ) -> Result { + Campaign::check_slug_availability(organisation_id, data.slug, &state.db).await?; + + Ok((StatusCode::OK, "Campaign slug is available")) + } + + pub async fn create_email_template( + Path(id): Path, + State(mut state): State, + _admin: OrganisationAdmin, + Json(request_body): Json + ) -> Result { + Organisation::create_email_template( + id, + request_body.name, + request_body.template, + &state.db, + state.snowflake_generator, + ).await?; + + Ok((StatusCode::OK, "Successfully created email template")) + } + + pub async fn get_all_email_templates( + _user: OrganisationAdmin, + Path(id): Path, + State(state): State + ) -> Result { + let email_templates = EmailTemplate::get_all_by_organisation(id, &state.db).await?; + + Ok((StatusCode::OK, Json(email_templates))) + } } diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index 290d7631..29b4124a 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -96,6 +96,8 @@ pub async fn app() -> Result { "/api/v1/organisation/:organisation_id", get(OrganisationHandler::get).delete(OrganisationHandler::delete), ) + .route("/api/v1/organisation/slug/:slug", + get(OrganisationHandler::get_by_slug)) .route( "/api/v1/organisation/:organisation_id/campaign", post(OrganisationHandler::create_campaign), diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index 5295e54e..3d350641 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -98,6 +98,10 @@ impl Campaign { } pub async fn check_slug_availability(organisation_id: i64, slug: String, pool: &Pool) -> Result<(), ChaosError> { + if !slug.is_ascii() { + return Err(ChaosError::BadRequest); + } + let exists = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM campaigns WHERE organisation_id = $1 AND slug = $2) diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 14ce224d..890a02cf 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -79,6 +79,10 @@ impl Organisation { mut snowflake_generator: SnowflakeIdGenerator, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { + if !slug.is_ascii() { + return Err(ChaosError::BadRequest); + } + let id = snowflake_generator.generate(); sqlx::query!( @@ -109,6 +113,10 @@ impl Organisation { } pub async fn check_slug_availability(slug: String, pool: &Pool) -> Result<(), ChaosError> { + if !slug.is_ascii() { + return Err(ChaosError::BadRequest); + } + let exists = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM organisations WHERE slug = $1) @@ -143,6 +151,22 @@ impl Organisation { Ok(organisation) } + pub async fn get_by_slug(slug: String, pool: &Pool) -> Result { + let organisation = sqlx::query_as!( + OrganisationDetails, + " + SELECT id, slug, name, logo, created_at + FROM organisations + WHERE slug = $1 + ", + slug + ) + .fetch_one(pool) + .await?; + + Ok(organisation) + } + pub async fn delete(id: i64, pool: &Pool) -> Result<(), ChaosError> { _ = sqlx::query!( " @@ -381,6 +405,10 @@ impl Organisation { pool: &Pool, mut snowflake_id_generator: SnowflakeIdGenerator, ) -> Result<(), ChaosError> { + if !slug.is_ascii() { + return Err(ChaosError::BadRequest); + } + let new_campaign_id = snowflake_id_generator.real_time_generate(); sqlx::query!( From b38f2fad53e20055d1773c05cb550ae7fc9f0535 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 26 Nov 2024 23:58:38 +1100 Subject: [PATCH 6/9] email templating and offer CRUD --- .../20241124054711_email_templates.sql | 12 ++ backend/migrations/20241126113027_offers.sql | 32 ++++ backend/server/Cargo.toml | 2 +- backend/server/src/handler/campaign.rs | 2 +- backend/server/src/handler/email_template.rs | 42 +++++ backend/server/src/handler/mod.rs | 1 + backend/server/src/handler/organisation.rs | 3 +- backend/server/src/models/auth.rs | 29 +-- backend/server/src/models/email_template.rs | 96 ++++++++++ backend/server/src/models/error.rs | 6 + backend/server/src/models/mod.rs | 2 + backend/server/src/models/offer.rs | 171 ++++++++++++++++++ backend/server/src/service/auth.rs | 2 +- backend/server/src/service/email_template.rs | 30 +++ backend/server/src/service/mod.rs | 1 + 15 files changed, 413 insertions(+), 18 deletions(-) create mode 100644 backend/migrations/20241124054711_email_templates.sql create mode 100644 backend/migrations/20241126113027_offers.sql create mode 100644 backend/server/src/handler/email_template.rs create mode 100644 backend/server/src/models/email_template.rs create mode 100644 backend/server/src/models/offer.rs create mode 100644 backend/server/src/service/email_template.rs diff --git a/backend/migrations/20241124054711_email_templates.sql b/backend/migrations/20241124054711_email_templates.sql new file mode 100644 index 00000000..393123c9 --- /dev/null +++ b/backend/migrations/20241124054711_email_templates.sql @@ -0,0 +1,12 @@ +CREATE TABLE email_templates ( + id BIGINT PRIMARY KEY, + organisation_id BIGINT NOT NULL, + name TEXT NOT NULL, + template TEXT NOT NULL, + CONSTRAINT FK_email_templates_organisations + FOREIGN KEY(organisation_id) + REFERENCES organisations(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE (organisation_id, name) +); \ No newline at end of file diff --git a/backend/migrations/20241126113027_offers.sql b/backend/migrations/20241126113027_offers.sql new file mode 100644 index 00000000..afe63925 --- /dev/null +++ b/backend/migrations/20241126113027_offers.sql @@ -0,0 +1,32 @@ +CREATE TYPE offer_status AS ENUM ('Draft', 'Sent', 'Accepted', 'Declined'); + +CREATE TABLE offers ( + id BIGINT PRIMARY KEY, + campaign_id BIGINT NOT NULL, + application_id BIGINT NOT NULL, + email_template_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + expiry TIMESTAMPTZ NOT NULL, + status offer_status NOT NULL DEFAULT 'Draft', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_offers_campaigns + FOREIGN KEY(campaign_id) + REFERENCES campaigns(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_offers_applications + FOREIGN KEY(application_id) + REFERENCES applications(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_offers_email_templates + FOREIGN KEY(email_template_id) + REFERENCES email_templates(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_offers_roles + FOREIGN KEY(role_id) + REFERENCES campaign_roles(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); \ No newline at end of file diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 9c4babe2..de3d9bb6 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -26,4 +26,4 @@ rust-s3 = "0.34.0" rs-snowflake = "0.6" jsonwebtoken = "9.1" dotenvy = "0.15" - +handlebars = "6.2" diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index c76ecb87..80223e9d 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -4,7 +4,7 @@ use crate::models::application::Application; use crate::models::application::NewApplication; use crate::models::auth::{AuthUser, SuperUser}; use crate::models::auth::CampaignAdmin; -use crate::models::campaign::{Campaign, CampaignSlugCheck}; +use crate::models::campaign::{Campaign}; use crate::models::error::ChaosError; use crate::models::role::{Role, RoleUpdate}; use crate::models::transaction::DBTransaction; diff --git a/backend/server/src/handler/email_template.rs b/backend/server/src/handler/email_template.rs new file mode 100644 index 00000000..c80557a9 --- /dev/null +++ b/backend/server/src/handler/email_template.rs @@ -0,0 +1,42 @@ +use axum::extract::{Path, State, Json}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use crate::models::app::AppState; +use crate::models::auth::EmailTemplateAdmin; +use crate::models::email_template::EmailTemplate; +use crate::models::error::ChaosError; +use crate::models::transaction::DBTransaction; + +pub struct EmailTemplateHandler; +impl EmailTemplateHandler { + pub async fn get( + mut transaction: DBTransaction<'_>, + Path(id): Path, + _user: EmailTemplateAdmin, + ) -> Result { + let email_template = EmailTemplate::get(id, &mut transaction.tx).await?; + + Ok((StatusCode::OK, Json(email_template))) + } + + pub async fn update( + _user: EmailTemplateAdmin, + Path(id): Path, + State(state): State, + Json(request_body): Json + ) -> Result { + EmailTemplate::update(id, request_body.name, request_body.template, &state.db).await?; + + Ok((StatusCode::OK, "Successfully updated email template")) + } + + pub async fn delete( + _user: EmailTemplateAdmin, + Path(id): Path, + State(state): State, + ) -> Result { + EmailTemplate::delete(id, &state.db).await?; + + Ok((StatusCode::OK, "Successfully delete email template")) + } +} \ No newline at end of file diff --git a/backend/server/src/handler/mod.rs b/backend/server/src/handler/mod.rs index 73a94627..5d8b5838 100644 --- a/backend/server/src/handler/mod.rs +++ b/backend/server/src/handler/mod.rs @@ -7,3 +7,4 @@ pub mod question; pub mod rating; pub mod role; pub mod user; +pub mod email_template; diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 423b4713..a2e80e0f 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -3,7 +3,7 @@ use crate::models::app::AppState; use crate::models::auth::SuperUser; use crate::models::auth::{AuthUser, OrganisationAdmin}; use crate::models::error::ChaosError; -use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck, CampaignSlugCheck}; +use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck}; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; @@ -22,6 +22,7 @@ impl OrganisationHandler { ) -> Result { Organisation::create( data.admin, + data.slug, data.name, state.snowflake_generator, &mut transaction.tx, diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 55dfb051..474b811d 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -58,7 +58,8 @@ where type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let user_id = extract_user_id_from_request(parts, app_state).await?; + let app_state = AppState::from_ref(state); + let user_id = extract_user_id_from_request(parts, &app_state).await?; Ok(AuthUser { user_id, @@ -81,7 +82,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; assert_is_super_user(user_id, &app_state.db).await?; @@ -105,7 +106,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let organisation_id = *parts .extract::>>() @@ -134,7 +135,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let campaign_id = *parts .extract::>>() @@ -163,7 +164,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let role_id = *parts .extract::>>() @@ -192,7 +193,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let Path(application_id) = parts .extract::>() @@ -227,7 +228,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let Path(application_id) = parts .extract::>() @@ -255,7 +256,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let Path(application_id) = parts .extract::>() @@ -283,7 +284,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let Path(rating_id) = parts .extract::>() @@ -310,7 +311,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let Path(rating_id) = parts .extract::>() @@ -337,7 +338,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id= extract_user_id_from_request(parts, app_state).await?; + let user_id= extract_user_id_from_request(parts, &app_state).await?; let question_id = *parts .extract::>>() @@ -366,7 +367,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let application_id = *parts .extract::>>() @@ -395,7 +396,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id = extract_user_id_from_request(parts, app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let application_id = *parts .extract::>>() @@ -424,7 +425,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id= extract_user_id_from_request(parts, app_state).await?; + let user_id= extract_user_id_from_request(parts, &app_state).await?; let template_id = *parts .extract::>>() diff --git a/backend/server/src/models/email_template.rs b/backend/server/src/models/email_template.rs new file mode 100644 index 00000000..cd84130a --- /dev/null +++ b/backend/server/src/models/email_template.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; +use std::ops::DerefMut; +use chrono::{DateTime, Local, Utc}; +use handlebars::Handlebars; +use serde::{Deserialize, Serialize}; +use snowflake::SnowflakeIdGenerator; +use sqlx::{Pool, Postgres, Transaction}; +use crate::models::application::Application; +use crate::models::campaign::Campaign; +use crate::models::error::ChaosError; + +/// Email templates to update applicants +/// Supported tags: +/// - `name` +/// - `role` +/// - `organisation_name` +/// - `expiry_date` +/// - `campaign_name` +#[derive(Deserialize, Serialize)] +pub struct EmailTemplate { + pub id: i64, + pub organisation_id: i64, + pub name: String, + pub template: String, +} + +impl EmailTemplate { + pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { + let template = sqlx::query_as!( + EmailTemplate, + "SELECT * FROM email_templates WHERE id = $1", + id + ) + .fetch_one(transaction.deref_mut()).await?; + + Ok(template) + } + + pub async fn get_all_by_organisation(organisation_id: i64, pool: &Pool) -> Result, ChaosError> { + let templates = sqlx::query_as!( + EmailTemplate, + "SELECT * FROM email_templates WHERE organisation_id = $1", + organisation_id + ) + .fetch_all(pool).await?; + + Ok(templates) + } + + pub async fn update(id: i64, name: String, template: String, pool: &Pool) -> Result<(), ChaosError> { + let _ = sqlx::query!( + " + UPDATE email_templates SET name = $2, template = $3 WHERE id = $1 RETURNING id + ", + id, name, template + ) + .fetch_one(pool).await?; + + Ok(()) + } + + pub async fn delete(id: i64, pool: &Pool) -> Result<(), ChaosError> { + let _ = sqlx::query!( + "DELETE FROM email_templates WHERE id = $1 RETURNING id", + id + ).fetch_one(pool).await?; + + Ok(()) + } + + pub async fn generate_email( + name: String, + role: String, + organisation_name: String, + campaign_name: String, + expiry_date: DateTime, + email_template_id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { + let template = EmailTemplate::get(email_template_id, transaction).await?; + + let mut handlebars = Handlebars::new(); + handlebars.register_template_string("template", template.template)?; + + let mut data = HashMap::new(); + data.insert("name", name); + data.insert("role", role); + data.insert("organisation_name", organisation_name); + data.insert("campaign_name", campaign_name); + data.insert("expiry_date", expiry_date.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()); + + let final_string = handlebars.render("template", &data)?; + + Ok(final_string) + } +} \ No newline at end of file diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index d541562b..25f3f794 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -39,6 +39,12 @@ pub enum ChaosError { #[error("DotEnvy error")] DotEnvyError(#[from] dotenvy::Error), + + #[error("Templating error")] + TemplateError(#[from] handlebars::TemplateError), + + #[error("Template rendering error")] + TemplateRendorError(#[from] handlebars::RenderError), } /// Implementation for converting errors into responses. Manages error code and message returned. diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index 9f30a8bb..c8f5f02e 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -11,3 +11,5 @@ pub mod role; pub mod storage; pub mod transaction; pub mod user; +pub mod email_template; +pub mod offer; diff --git a/backend/server/src/models/offer.rs b/backend/server/src/models/offer.rs new file mode 100644 index 00000000..5a0ac9ca --- /dev/null +++ b/backend/server/src/models/offer.rs @@ -0,0 +1,171 @@ +use std::ops::DerefMut; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use snowflake::SnowflakeIdGenerator; +use sqlx::{Postgres, Transaction}; +use crate::models::application::Application; +use crate::models::campaign::Campaign; +use crate::models::email_template::EmailTemplate; +use crate::models::error::ChaosError; + +#[derive(Deserialize)] +pub struct Offer { + id: i64, + campaign_id: i64, + application_id: i64, + email_template_id: i64, + role_id: i64, + expiry: DateTime, + status: OfferStatus, + created_at: DateTime, +} + +#[derive(Deserialize, Serialize)] +pub struct OfferDetails { + id: i64, + campaign_id: i64, + organisation_name: String, + campaign_name: String, + application_id: i64, + user_id: i64, + user_name: String, + user_email: String, + email_template_id: i64, + role_id: i64, + role_name: String, + expiry: DateTime, + status: OfferStatus, + created_at: DateTime, +} + +#[derive(Deserialize, Serialize, sqlx::Type, Clone, Debug)] +#[sqlx(type_name = "offer_status", rename_all = "PascalCase")] +pub enum OfferStatus { + Draft, + Sent, + Accepted, + Declined +} + +impl Offer { + pub async fn create(campaign_id: i64, application_id: i64, email_template_id: i64, role_id: i64, expiry: DateTime, transaction: &mut Transaction<'_, Postgres>, mut snowflake_id_generator: SnowflakeIdGenerator) -> Result<(), ChaosError> { + let id = snowflake_id_generator.real_time_generate(); + + let _ = sqlx::query!( + " + INSERT INTO offers (id, campaign_id, application_id, email_template_id, role_id, expiry) VALUES ($1, $2, $3, $4, $5, $6) + ", + id, + campaign_id, + application_id, + email_template_id, + role_id, + expiry + ) + .execute(transaction.deref_mut()) + .await?; + + Ok(()) + } + + pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { + let offer = sqlx::query_as!( + OfferDetails, + r#" + SELECT + off.id, off.campaign_id, off.application_id, off.email_template_id, + off.role_id, off.expiry, off.created_at, + off.status AS "status!: OfferStatus", + c.name as campaign_name, + o.name as organisation_name, + u.id as user_id, + u.name as user_name, + u.email as user_email, + r.name as role_name + FROM offers off + JOIN campaigns c ON c.id = off.campaign_id + JOIN organisations o ON o.id = c.organisation_id + JOIN applications a ON a.id = off.application_id + JOIN users u ON u.id = a.user_id + JOIN campaign_roles r ON r.id = off.role_id + WHERE off.id = $1 + "#, + id + ) + .fetch_one(transaction.deref_mut()) + .await?; + + Ok(offer) + } + + pub async fn get_by_campaign(campaign_id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result, ChaosError> { + let offers = sqlx::query_as!( + OfferDetails, + r#" + SELECT + off.id, off.campaign_id, off.application_id, off.email_template_id, + off.role_id, off.expiry, off.created_at, + off.status AS "status!: OfferStatus", + c.name as campaign_name, + o.name as organisation_name, + u.id as user_id, + u.name as user_name, + u.email as user_email, + r.name as role_name + FROM offers off + JOIN campaigns c on c.id = off.campaign_id + JOIN organisations o on o.id = c.organisation_id + JOIN applications a ON a.id = off.application_id + JOIN users u on u.id = a.user_id + JOIN campaign_roles r on r.id = off.role_id + WHERE off.id = $1 + "#, + campaign_id + ) + .fetch_all(transaction.deref_mut()) + .await?; + + Ok(offers) + } + + pub async fn delete(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + let _ = sqlx::query!("DELETE FROM offers WHERE id = $1 RETURNING id", id) + .fetch_one(transaction.deref_mut()) + .await?; + + Ok(()) + } + + pub async fn reply(id: i64, accepting_user_id: i64, accept: bool, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + let offer = Offer::get(id, transaction).await?; + + if Utc::now() > offer.expiry || accepting_user_id != offer.user_id { + return Err(ChaosError::BadRequest) + } + + let mut status = OfferStatus::Accepted; + if !accept { + status = OfferStatus::Declined; + } + + let _ = sqlx::query!("UPDATE offers SET status = $2 WHERE id = $1", id, status as OfferStatus) + .execute(transaction.deref_mut()) + .await?; + + Ok(()) + } + + pub async fn preview_email(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { + let offer = Offer::get(id, transaction).await?; + let email = EmailTemplate::generate_email(offer.user_name, offer.role_name, offer.organisation_name, offer.campaign_name, offer.expiry, offer.email_template_id, transaction).await?; + Ok(email) + } + + pub async fn send_offer(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + let offer = Offer::get(id, transaction).await?; + let email = EmailTemplate::generate_email(offer.user_name, offer.role_name, offer.organisation_name, offer.campaign_name, offer.expiry, offer.email_template_id, transaction).await?; + + // TODO: Send email e.g. send_email(offer.user_email, email).await?; + Ok(()) + } +} \ No newline at end of file diff --git a/backend/server/src/service/auth.rs b/backend/server/src/service/auth.rs index d0de71ed..4d4ee154 100644 --- a/backend/server/src/service/auth.rs +++ b/backend/server/src/service/auth.rs @@ -62,7 +62,7 @@ pub async fn assert_is_super_user(user_id: i64, pool: &Pool) -> Result Ok(()) } -pub async fn extract_user_id_from_request(parts: &mut Parts, state: AppState) -> Result where S: Send + Sync { +pub async fn extract_user_id_from_request(parts: &mut Parts, state: &AppState) -> Result { let decoding_key = &state.decoding_key; let jwt_validator = &state.jwt_validator; let TypedHeader(cookies) = parts diff --git a/backend/server/src/service/email_template.rs b/backend/server/src/service/email_template.rs new file mode 100644 index 00000000..9c219fdb --- /dev/null +++ b/backend/server/src/service/email_template.rs @@ -0,0 +1,30 @@ +use crate::models::error::ChaosError; +use sqlx::{Pool, Postgres}; + +pub async fn user_is_email_template_admin( + user_id: i64, + template_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let is_admin = sqlx::query!( + " + SELECT EXISTS( + SELECT 1 FROM email_templates et + JOIN organisation_members m on et.organisation_id = m.organisation_id + WHERE et.organisation_id = $1 AND m.user_id = $2 AND m.role = 'Admin' + ) + ", + template_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(()) +} diff --git a/backend/server/src/service/mod.rs b/backend/server/src/service/mod.rs index 8fd957f6..98720276 100644 --- a/backend/server/src/service/mod.rs +++ b/backend/server/src/service/mod.rs @@ -8,3 +8,4 @@ pub mod organisation; pub mod question; pub mod rating; pub mod role; +pub mod email_template; \ No newline at end of file From 534df8da08696ce2db1c0c91ed58feb5d839aea4 Mon Sep 17 00:00:00 2001 From: Kavika Date: Wed, 27 Nov 2024 00:19:11 +1100 Subject: [PATCH 7/9] fix email_template auth service join --- backend/server/src/service/email_template.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server/src/service/email_template.rs b/backend/server/src/service/email_template.rs index 9c219fdb..c6ad5d31 100644 --- a/backend/server/src/service/email_template.rs +++ b/backend/server/src/service/email_template.rs @@ -11,7 +11,7 @@ pub async fn user_is_email_template_admin( SELECT EXISTS( SELECT 1 FROM email_templates et JOIN organisation_members m on et.organisation_id = m.organisation_id - WHERE et.organisation_id = $1 AND m.user_id = $2 AND m.role = 'Admin' + WHERE et.id = $1 AND m.user_id = $2 AND m.role = 'Admin' ) ", template_id, From 9d346e01cb50b85ebdd11d9476a043adaeea8c1b Mon Sep 17 00:00:00 2001 From: Kavika Date: Wed, 27 Nov 2024 00:27:19 +1100 Subject: [PATCH 8/9] offer CRUD --- backend/server/src/handler/campaign.rs | 25 ++++++++++ backend/server/src/handler/mod.rs | 1 + backend/server/src/handler/offer.rs | 67 ++++++++++++++++++++++++++ backend/server/src/models/app.rs | 21 ++++++++ backend/server/src/models/auth.rs | 62 +++++++++++++++++++++++- backend/server/src/models/offer.rs | 57 ++++++++++++---------- backend/server/src/service/mod.rs | 3 +- backend/server/src/service/offer.rs | 47 ++++++++++++++++++ 8 files changed, 255 insertions(+), 28 deletions(-) create mode 100644 backend/server/src/handler/offer.rs create mode 100644 backend/server/src/service/offer.rs diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index 80223e9d..30316ee2 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -11,6 +11,7 @@ use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; +use crate::models::offer::Offer; use crate::models::organisation::{Organisation, SlugCheck}; pub struct CampaignHandler; @@ -126,4 +127,28 @@ impl CampaignHandler { transaction.tx.commit().await?; Ok((StatusCode::OK, Json(applications))) } + + pub async fn create_offer( + Path(id): Path, + State(state): State, + _admin: CampaignAdmin, + mut transaction: DBTransaction<'_>, + Json(data): Json + ) -> Result { + let _ = Offer::create(id, data.application_id, data.email_template_id, data.role_id, data.expiry, &mut transaction.tx, state.snowflake_generator).await?; + transaction.tx.commit().await?; + + Ok((StatusCode::OK, "Successfully created offer")) + } + + pub async fn get_offers( + mut transaction: DBTransaction<'_>, + Path(id): Path, + _user: CampaignAdmin + ) -> Result { + let offers = Offer::get_by_campaign(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + + Ok((StatusCode::OK, Json(offers))) + } } diff --git a/backend/server/src/handler/mod.rs b/backend/server/src/handler/mod.rs index 5d8b5838..037cbbc2 100644 --- a/backend/server/src/handler/mod.rs +++ b/backend/server/src/handler/mod.rs @@ -8,3 +8,4 @@ pub mod rating; pub mod role; pub mod user; pub mod email_template; +pub mod offer; diff --git a/backend/server/src/handler/offer.rs b/backend/server/src/handler/offer.rs new file mode 100644 index 00000000..6bb0fc9e --- /dev/null +++ b/backend/server/src/handler/offer.rs @@ -0,0 +1,67 @@ +use axum::extract::{Path, Json}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use crate::models::auth::{AuthUser, CampaignAdmin, OfferAdmin, OfferRecipient}; +use crate::models::error::ChaosError; +use crate::models::offer::{Offer, OfferReply}; +use crate::models::transaction::DBTransaction; + + +pub struct OfferHandler; +impl OfferHandler { + pub async fn get( + mut transaction: DBTransaction<'_>, + Path(id): Path, + _user: OfferAdmin, + ) -> Result { + let offer = Offer::get(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + + Ok((StatusCode::OK, Json(offer))) + } + + pub async fn delete( + mut transaction: DBTransaction<'_>, + Path(id): Path, + _user: OfferAdmin + ) -> Result { + Offer::delete(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + + Ok((StatusCode::OK, "Successfully deleted offer")) + } + + pub async fn reply( + mut transaction: DBTransaction<'_>, + Path(id): Path, + _user: OfferRecipient, + Json(reply): Json, + ) -> Result { + Offer::reply(id, reply.accept, &mut transaction.tx).await?; + transaction.tx.commit().await?; + + Ok((StatusCode::OK, "Successfully accepted offer")) + } + + pub async fn preview_email( + mut transaction: DBTransaction<'_>, + Path(id): Path, + _user: OfferAdmin, + ) -> Result { + let string = Offer::preview_email(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + + Ok((StatusCode::OK, string)) + } + + pub async fn send_offer( + mut transaction: DBTransaction<'_>, + Path(id): Path, + _user: OfferAdmin, + ) -> Result { + Offer::send_offer(id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + + Ok((StatusCode::OK, "Successfully sent offer")) + } +} \ No newline at end of file diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index 29b4124a..91ae5f26 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -19,6 +19,7 @@ use sqlx::postgres::PgPoolOptions; use sqlx::{Pool, Postgres}; use std::env; use crate::handler::email_template::EmailTemplateHandler; +use crate::handler::offer::OfferHandler; use crate::models::organisation::Organisation; #[derive(Clone)] @@ -205,6 +206,14 @@ pub async fn app() -> Result { "/api/v1/campaign/:campaign_id/application", post(CampaignHandler::create_application), ) + .route( + "/api/v1/campaign/:campaign_id/offer", + post(CampaignHandler::create_offer) + ) + .route( + "/api/v1/campaign/:campaign_id/offers", + get(CampaignHandler::get_offers) + ) .route( "/api/v1/application/:application_id", get(ApplicationHandler::get), @@ -239,5 +248,17 @@ pub async fn app() -> Result { .patch(EmailTemplateHandler::update) .delete(EmailTemplateHandler::delete) ) + .route( + "/api/v1/offer/:offer_id", + get(OfferHandler::get).delete(OfferHandler::delete).post(OfferHandler::reply) + ) + .route( + "/api/v1/offer/:offer_id/preview", + get(OfferHandler::preview_email) + ) + .route( + "/api/v1/offer/:offer_id/send", + post(OfferHandler::send_offer) + ) .with_state(state)) } diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 474b811d..06be344c 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -19,7 +19,9 @@ use axum::{async_trait, RequestPartsExt}; use axum_extra::{headers::Cookie, TypedHeader}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::models::offer::Offer; use crate::service::email_template::user_is_email_template_admin; +use crate::service::offer::{assert_user_is_offer_admin, assert_user_is_offer_recipient}; // tells the web framework how to take the url query params they will have #[derive(Deserialize, Serialize)] @@ -431,7 +433,7 @@ where .extract::>>() .await .map_err(|_| ChaosError::BadRequest)? - .get("application_id") + .get("template_id") .ok_or(ChaosError::BadRequest)?; user_is_email_template_admin(user_id, template_id, &app_state.db).await?; @@ -439,3 +441,61 @@ where Ok(EmailTemplateAdmin { user_id }) } } + +pub struct OfferAdmin { + pub user_id: i64, +} + +#[async_trait] +impl FromRequestParts for OfferAdmin +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 user_id= extract_user_id_from_request(parts, &app_state).await?; + + let offer_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("offer_id") + .ok_or(ChaosError::BadRequest)?; + + assert_user_is_offer_admin(user_id, offer_id, &app_state.db).await?; + + Ok(OfferAdmin { user_id }) + } +} + +pub struct OfferRecipient { + pub user_id: i64, +} + +#[async_trait] +impl FromRequestParts for OfferRecipient +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 user_id= extract_user_id_from_request(parts, &app_state).await?; + + let offer_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("offer_id") + .ok_or(ChaosError::BadRequest)?; + + assert_user_is_offer_recipient(user_id, offer_id, &app_state.db).await?; + + Ok(OfferRecipient { user_id }) + } +} diff --git a/backend/server/src/models/offer.rs b/backend/server/src/models/offer.rs index 5a0ac9ca..52725601 100644 --- a/backend/server/src/models/offer.rs +++ b/backend/server/src/models/offer.rs @@ -10,32 +10,32 @@ use crate::models::error::ChaosError; #[derive(Deserialize)] pub struct Offer { - id: i64, - campaign_id: i64, - application_id: i64, - email_template_id: i64, - role_id: i64, - expiry: DateTime, - status: OfferStatus, - created_at: DateTime, + pub id: i64, + pub campaign_id: i64, + pub application_id: i64, + pub email_template_id: i64, + pub role_id: i64, + pub expiry: DateTime, + pub status: OfferStatus, + pub created_at: DateTime, } #[derive(Deserialize, Serialize)] pub struct OfferDetails { - id: i64, - campaign_id: i64, - organisation_name: String, - campaign_name: String, - application_id: i64, - user_id: i64, - user_name: String, - user_email: String, - email_template_id: i64, - role_id: i64, - role_name: String, - expiry: DateTime, - status: OfferStatus, - created_at: DateTime, + pub id: i64, + pub campaign_id: i64, + pub organisation_name: String, + pub campaign_name: String, + pub application_id: i64, + pub user_id: i64, + pub user_name: String, + pub user_email: String, + pub email_template_id: i64, + pub role_id: i64, + pub role_name: String, + pub expiry: DateTime, + pub status: OfferStatus, + pub created_at: DateTime, } #[derive(Deserialize, Serialize, sqlx::Type, Clone, Debug)] @@ -47,8 +47,13 @@ pub enum OfferStatus { Declined } +#[derive(Deserialize)] +pub struct OfferReply { + pub accept: bool, +} + impl Offer { - pub async fn create(campaign_id: i64, application_id: i64, email_template_id: i64, role_id: i64, expiry: DateTime, transaction: &mut Transaction<'_, Postgres>, mut snowflake_id_generator: SnowflakeIdGenerator) -> Result<(), ChaosError> { + pub async fn create(campaign_id: i64, application_id: i64, email_template_id: i64, role_id: i64, expiry: DateTime, transaction: &mut Transaction<'_, Postgres>, mut snowflake_id_generator: SnowflakeIdGenerator) -> Result { let id = snowflake_id_generator.real_time_generate(); let _ = sqlx::query!( @@ -65,7 +70,7 @@ impl Offer { .execute(transaction.deref_mut()) .await?; - Ok(()) + Ok(id) } pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { @@ -136,10 +141,10 @@ impl Offer { Ok(()) } - pub async fn reply(id: i64, accepting_user_id: i64, accept: bool, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + pub async fn reply(id: i64, accept: bool, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { let offer = Offer::get(id, transaction).await?; - if Utc::now() > offer.expiry || accepting_user_id != offer.user_id { + if Utc::now() > offer.expiry { return Err(ChaosError::BadRequest) } diff --git a/backend/server/src/service/mod.rs b/backend/server/src/service/mod.rs index 98720276..53f76596 100644 --- a/backend/server/src/service/mod.rs +++ b/backend/server/src/service/mod.rs @@ -8,4 +8,5 @@ pub mod organisation; pub mod question; pub mod rating; pub mod role; -pub mod email_template; \ No newline at end of file +pub mod email_template; +pub mod offer; \ No newline at end of file diff --git a/backend/server/src/service/offer.rs b/backend/server/src/service/offer.rs new file mode 100644 index 00000000..1a639441 --- /dev/null +++ b/backend/server/src/service/offer.rs @@ -0,0 +1,47 @@ +use crate::models::error::ChaosError; +use sqlx::{Pool, Postgres}; +use crate::models::offer::Offer; + +pub async fn assert_user_is_offer_admin( + user_id: i64, + offer_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let is_admin = sqlx::query!( + " + SELECT EXISTS( + SELECT 1 FROM offers off + JOIN campaigns c ON c.id = off.campaign_id + JOIN organisation_members m on c.organisation_id = m.organisation_id + WHERE off.id = $1 AND m.user_id = $2 AND m.role = 'Admin' + ) + ", + offer_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(()) +} + +pub async fn assert_user_is_offer_recipient( + user_id: i64, + offer_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let tx = &mut pool.begin().await?; + let offer = Offer::get(offer_id, tx).await?; + + if offer.user_id != user_id { + return Err(ChaosError::Unauthorized) + } + + Ok(()) +} \ No newline at end of file From 6a82a2cde60dea252119aa3abfad637565084851 Mon Sep 17 00:00:00 2001 From: Kavika Date: Wed, 27 Nov 2024 00:33:33 +1100 Subject: [PATCH 9/9] ran `cargo fmt` & remove unused imports --- backend/server/src/handler/answer.rs | 10 +-- backend/server/src/handler/campaign.rs | 28 ++++-- backend/server/src/handler/email_template.rs | 10 +-- backend/server/src/handler/mod.rs | 4 +- backend/server/src/handler/offer.rs | 13 ++- backend/server/src/handler/organisation.rs | 19 ++-- backend/server/src/handler/rating.rs | 3 +- backend/server/src/models/answer.rs | 10 +-- backend/server/src/models/app.rs | 40 +++++---- backend/server/src/models/auth.rs | 29 +++--- backend/server/src/models/campaign.rs | 42 ++++++--- backend/server/src/models/email_template.rs | 56 ++++++++---- backend/server/src/models/mod.rs | 4 +- backend/server/src/models/offer.rs | 95 +++++++++++++++----- backend/server/src/models/organisation.rs | 44 +++++---- backend/server/src/models/role.rs | 2 +- backend/server/src/service/auth.rs | 20 +++-- backend/server/src/service/email_template.rs | 8 +- backend/server/src/service/mod.rs | 4 +- backend/server/src/service/offer.rs | 14 +-- 20 files changed, 280 insertions(+), 175 deletions(-) diff --git a/backend/server/src/handler/answer.rs b/backend/server/src/handler/answer.rs index 34f3e0ea..065be1da 100644 --- a/backend/server/src/handler/answer.rs +++ b/backend/server/src/handler/answer.rs @@ -1,6 +1,6 @@ use crate::models::answer::{Answer, NewAnswer}; use crate::models::app::AppState; -use crate::models::auth::{AnswerOwner, ApplicationOwner, AuthUser}; +use crate::models::auth::{AnswerOwner, ApplicationOwner}; use crate::models::error::ChaosError; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; @@ -13,14 +13,14 @@ pub struct AnswerHandler; impl AnswerHandler { pub async fn create( State(state): State, - Path(path): Path, - user: AuthUser, + Path(application_id): Path, + _user: ApplicationOwner, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { + // TODO: Check whether the question is contained in the campaign being applied to let id = Answer::create( - user.user_id, - data.application_id, + application_id, data.question_id, data.answer_data, state.snowflake_generator, diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index 30316ee2..0e1db393 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -2,17 +2,16 @@ use crate::models; use crate::models::app::AppState; use crate::models::application::Application; use crate::models::application::NewApplication; -use crate::models::auth::{AuthUser, SuperUser}; +use crate::models::auth::AuthUser; use crate::models::auth::CampaignAdmin; -use crate::models::campaign::{Campaign}; +use crate::models::campaign::Campaign; use crate::models::error::ChaosError; +use crate::models::offer::Offer; use crate::models::role::{Role, RoleUpdate}; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; -use crate::models::offer::Offer; -use crate::models::organisation::{Organisation, SlugCheck}; pub struct CampaignHandler; impl CampaignHandler { @@ -31,7 +30,8 @@ impl CampaignHandler { Path((organisation_slug, campaign_slug)): Path<(String, String)>, _user: AuthUser, ) -> Result { - let campaign = Campaign::get_by_slugs(organisation_slug, campaign_slug, &mut transaction.tx).await?; + let campaign = + Campaign::get_by_slugs(organisation_slug, campaign_slug, &mut transaction.tx).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, Json(campaign))) } @@ -62,7 +62,8 @@ impl CampaignHandler { Path(id): Path, _admin: CampaignAdmin, ) -> Result { - let banner_url = Campaign::update_banner(id, &mut transaction.tx, &state.storage_bucket).await?; + let banner_url = + Campaign::update_banner(id, &mut transaction.tx, &state.storage_bucket).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, Json(banner_url))) } @@ -133,9 +134,18 @@ impl CampaignHandler { State(state): State, _admin: CampaignAdmin, mut transaction: DBTransaction<'_>, - Json(data): Json + Json(data): Json, ) -> Result { - let _ = Offer::create(id, data.application_id, data.email_template_id, data.role_id, data.expiry, &mut transaction.tx, state.snowflake_generator).await?; + let _ = Offer::create( + id, + data.application_id, + data.email_template_id, + data.role_id, + data.expiry, + &mut transaction.tx, + state.snowflake_generator, + ) + .await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully created offer")) @@ -144,7 +154,7 @@ impl CampaignHandler { pub async fn get_offers( mut transaction: DBTransaction<'_>, Path(id): Path, - _user: CampaignAdmin + _user: CampaignAdmin, ) -> Result { let offers = Offer::get_by_campaign(id, &mut transaction.tx).await?; transaction.tx.commit().await?; diff --git a/backend/server/src/handler/email_template.rs b/backend/server/src/handler/email_template.rs index c80557a9..e392834f 100644 --- a/backend/server/src/handler/email_template.rs +++ b/backend/server/src/handler/email_template.rs @@ -1,11 +1,11 @@ -use axum::extract::{Path, State, Json}; -use axum::http::StatusCode; -use axum::response::IntoResponse; use crate::models::app::AppState; use crate::models::auth::EmailTemplateAdmin; use crate::models::email_template::EmailTemplate; use crate::models::error::ChaosError; use crate::models::transaction::DBTransaction; +use axum::extract::{Json, Path, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; pub struct EmailTemplateHandler; impl EmailTemplateHandler { @@ -23,7 +23,7 @@ impl EmailTemplateHandler { _user: EmailTemplateAdmin, Path(id): Path, State(state): State, - Json(request_body): Json + Json(request_body): Json, ) -> Result { EmailTemplate::update(id, request_body.name, request_body.template, &state.db).await?; @@ -39,4 +39,4 @@ impl EmailTemplateHandler { Ok((StatusCode::OK, "Successfully delete email template")) } -} \ No newline at end of file +} diff --git a/backend/server/src/handler/mod.rs b/backend/server/src/handler/mod.rs index 037cbbc2..33675427 100644 --- a/backend/server/src/handler/mod.rs +++ b/backend/server/src/handler/mod.rs @@ -2,10 +2,10 @@ pub mod answer; pub mod application; pub mod auth; pub mod campaign; +pub mod email_template; +pub mod offer; pub mod organisation; pub mod question; pub mod rating; pub mod role; pub mod user; -pub mod email_template; -pub mod offer; diff --git a/backend/server/src/handler/offer.rs b/backend/server/src/handler/offer.rs index 6bb0fc9e..92297c63 100644 --- a/backend/server/src/handler/offer.rs +++ b/backend/server/src/handler/offer.rs @@ -1,11 +1,10 @@ -use axum::extract::{Path, Json}; -use axum::http::StatusCode; -use axum::response::IntoResponse; -use crate::models::auth::{AuthUser, CampaignAdmin, OfferAdmin, OfferRecipient}; +use crate::models::auth::{OfferAdmin, OfferRecipient}; use crate::models::error::ChaosError; use crate::models::offer::{Offer, OfferReply}; use crate::models::transaction::DBTransaction; - +use axum::extract::{Json, Path}; +use axum::http::StatusCode; +use axum::response::IntoResponse; pub struct OfferHandler; impl OfferHandler { @@ -23,7 +22,7 @@ impl OfferHandler { pub async fn delete( mut transaction: DBTransaction<'_>, Path(id): Path, - _user: OfferAdmin + _user: OfferAdmin, ) -> Result { Offer::delete(id, &mut transaction.tx).await?; transaction.tx.commit().await?; @@ -64,4 +63,4 @@ impl OfferHandler { Ok((StatusCode::OK, "Successfully sent offer")) } -} \ No newline at end of file +} diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index a2e80e0f..7950f1fb 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -2,14 +2,16 @@ use crate::models; use crate::models::app::AppState; use crate::models::auth::SuperUser; use crate::models::auth::{AuthUser, OrganisationAdmin}; +use crate::models::campaign::Campaign; +use crate::models::email_template::EmailTemplate; use crate::models::error::ChaosError; -use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck}; +use crate::models::organisation::{ + AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck, +}; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; -use crate::models::campaign::{Campaign}; -use crate::models::email_template::EmailTemplate; pub struct OrganisationHandler; @@ -163,7 +165,7 @@ impl OrganisationHandler { pub async fn create_campaign( Path(id): Path, - State(mut state): State, + State(state): State, _admin: OrganisationAdmin, Json(request_body): Json, ) -> Result { @@ -195,9 +197,9 @@ impl OrganisationHandler { pub async fn create_email_template( Path(id): Path, - State(mut state): State, + State(state): State, _admin: OrganisationAdmin, - Json(request_body): Json + Json(request_body): Json, ) -> Result { Organisation::create_email_template( id, @@ -205,7 +207,8 @@ impl OrganisationHandler { request_body.template, &state.db, state.snowflake_generator, - ).await?; + ) + .await?; Ok((StatusCode::OK, "Successfully created email template")) } @@ -213,7 +216,7 @@ impl OrganisationHandler { pub async fn get_all_email_templates( _user: OrganisationAdmin, Path(id): Path, - State(state): State + State(state): State, ) -> Result { let email_templates = EmailTemplate::get_all_by_organisation(id, &state.db).await?; diff --git a/backend/server/src/handler/rating.rs b/backend/server/src/handler/rating.rs index 2ec36960..5c93e203 100644 --- a/backend/server/src/handler/rating.rs +++ b/backend/server/src/handler/rating.rs @@ -1,7 +1,6 @@ use crate::models::app::AppState; use crate::models::auth::{ - ApplicationCreatorGivenApplicationId, ApplicationReviewerGivenApplicationId, - ApplicationReviewerGivenRatingId, RatingCreator, + ApplicationReviewerGivenApplicationId, ApplicationReviewerGivenRatingId, RatingCreator, }; use crate::models::error::ChaosError; use crate::models::rating::{NewRating, Rating}; diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index 670a7ba8..c9f51772 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -1,11 +1,9 @@ use crate::models::error::ChaosError; -use crate::models::question::{ - MultiOptionData, MultiOptionQuestionOption, QuestionData, QuestionType, QuestionTypeParent, -}; +use crate::models::question::QuestionType; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; -use sqlx::{Pool, Postgres, Transaction}; +use sqlx::{Postgres, Transaction}; use std::ops::DerefMut; /// The `Answer` type that will be sent in API responses. @@ -64,7 +62,6 @@ pub struct AnswerTypeApplicationId { impl Answer { pub async fn create( - user_id: i64, application_id: i64, question_id: i64, answer_data: AnswerData, @@ -379,9 +376,6 @@ impl AnswerData { let options = ranking_answers.expect("Data should exist for Ranking variant"); AnswerData::Ranking(options) } - _ => { - AnswerData::ShortAnswer("".to_string()) // Should never be reached, hence return ShortAnswer - } }; } diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index 91ae5f26..5fd59143 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -2,6 +2,8 @@ use crate::handler::answer::AnswerHandler; use crate::handler::application::ApplicationHandler; use crate::handler::auth::google_callback; use crate::handler::campaign::CampaignHandler; +use crate::handler::email_template::EmailTemplateHandler; +use crate::handler::offer::OfferHandler; use crate::handler::organisation::OrganisationHandler; use crate::handler::question::QuestionHandler; use crate::handler::rating::RatingHandler; @@ -18,9 +20,6 @@ use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use sqlx::{Pool, Postgres}; use std::env; -use crate::handler::email_template::EmailTemplateHandler; -use crate::handler::offer::OfferHandler; -use crate::models::organisation::Organisation; #[derive(Clone)] pub struct AppState { @@ -92,20 +91,25 @@ pub async fn app() -> Result { get(ApplicationHandler::get_from_curr_user), ) .route("/api/v1/organisation", post(OrganisationHandler::create)) - .route("/api/v1/organisation/slug_check", post(OrganisationHandler::check_organisation_slug_availability)) + .route( + "/api/v1/organisation/slug_check", + post(OrganisationHandler::check_organisation_slug_availability), + ) .route( "/api/v1/organisation/:organisation_id", get(OrganisationHandler::get).delete(OrganisationHandler::delete), ) - .route("/api/v1/organisation/slug/:slug", - get(OrganisationHandler::get_by_slug)) + .route( + "/api/v1/organisation/slug/:slug", + get(OrganisationHandler::get_by_slug), + ) .route( "/api/v1/organisation/:organisation_id/campaign", post(OrganisationHandler::create_campaign), ) .route( "/api/v1/organisation/:organisation_id/campaign/slug_check", - post(OrganisationHandler::check_campaign_slug_availability) + post(OrganisationHandler::check_campaign_slug_availability), ) .route( "/api/v1/organisation/:organisation_id/campaigns", @@ -113,11 +117,11 @@ pub async fn app() -> Result { ) .route( "/api/v1/organisation/:organisation_id/email_template", - post(OrganisationHandler::create_email_template) + post(OrganisationHandler::create_email_template), ) .route( "/api/v1/organisation/:organisation_id/email_templates", - get(OrganisationHandler::get_all_email_templates) + get(OrganisationHandler::get_all_email_templates), ) .route( "/api/v1/organisation/:organisation_id/logo", @@ -183,7 +187,7 @@ pub async fn app() -> Result { ) .route( "/api/v1/campaign/slug/:organisation_slug/:campaign_slug", - get(CampaignHandler::get_by_slugs) + get(CampaignHandler::get_by_slugs), ) .route("/api/v1/campaign", get(CampaignHandler::get_all)) .route( @@ -208,11 +212,11 @@ pub async fn app() -> Result { ) .route( "/api/v1/campaign/:campaign_id/offer", - post(CampaignHandler::create_offer) + post(CampaignHandler::create_offer), ) .route( "/api/v1/campaign/:campaign_id/offers", - get(CampaignHandler::get_offers) + get(CampaignHandler::get_offers), ) .route( "/api/v1/application/:application_id", @@ -231,7 +235,7 @@ pub async fn app() -> Result { get(AnswerHandler::get_all_common_by_application), ) .route( - "/api/v1/application/:applicaiton_id/answer", + "/api/v1/application/:application_id/answer", post(AnswerHandler::create), ) .route( @@ -246,19 +250,21 @@ pub async fn app() -> Result { "/api/v1/email_template/:template_id", get(EmailTemplateHandler::get) .patch(EmailTemplateHandler::update) - .delete(EmailTemplateHandler::delete) + .delete(EmailTemplateHandler::delete), ) .route( "/api/v1/offer/:offer_id", - get(OfferHandler::get).delete(OfferHandler::delete).post(OfferHandler::reply) + get(OfferHandler::get) + .delete(OfferHandler::delete) + .post(OfferHandler::reply), ) .route( "/api/v1/offer/:offer_id/preview", - get(OfferHandler::preview_email) + get(OfferHandler::preview_email), ) .route( "/api/v1/offer/:offer_id/send", - post(OfferHandler::send_offer) + post(OfferHandler::send_offer), ) .with_state(state)) } diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 06be344c..38902d2b 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -4,7 +4,8 @@ use crate::service::answer::user_is_answer_owner; use crate::service::application::{user_is_application_admin, user_is_application_owner}; use crate::service::auth::{assert_is_super_user, extract_user_id_from_request}; use crate::service::campaign::user_is_campaign_admin; -use crate::service::jwt::decode_auth_token; +use crate::service::email_template::user_is_email_template_admin; +use crate::service::offer::{assert_user_is_offer_admin, assert_user_is_offer_recipient}; use crate::service::organisation::assert_user_is_organisation_admin; use crate::service::question::user_is_question_admin; use crate::service::rating::{ @@ -16,12 +17,8 @@ use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; use axum::{async_trait, RequestPartsExt}; -use axum_extra::{headers::Cookie, TypedHeader}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::models::offer::Offer; -use crate::service::email_template::user_is_email_template_admin; -use crate::service::offer::{assert_user_is_offer_admin, assert_user_is_offer_recipient}; // tells the web framework how to take the url query params they will have #[derive(Deserialize, Serialize)] @@ -63,9 +60,7 @@ where let app_state = AppState::from_ref(state); let user_id = extract_user_id_from_request(parts, &app_state).await?; - Ok(AuthUser { - user_id, - }) + Ok(AuthUser { user_id }) } } @@ -88,9 +83,7 @@ where assert_is_super_user(user_id, &app_state.db).await?; - Ok(SuperUser { - user_id, - }) + Ok(SuperUser { user_id }) } } @@ -293,7 +286,8 @@ where .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_application_reviewer_given_rating_id(user_id, rating_id, &app_state.db).await?; + assert_user_is_application_reviewer_given_rating_id(user_id, rating_id, &app_state.db) + .await?; Ok(ApplicationReviewerGivenRatingId { user_id }) } @@ -320,7 +314,8 @@ where .await .map_err(|_| ChaosError::BadRequest)?; - assert_user_is_rating_creator_and_organisation_member(user_id, rating_id, &app_state.db).await?; + assert_user_is_rating_creator_and_organisation_member(user_id, rating_id, &app_state.db) + .await?; Ok(RatingCreator { user_id }) } @@ -340,7 +335,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id= extract_user_id_from_request(parts, &app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let question_id = *parts .extract::>>() @@ -427,7 +422,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id= extract_user_id_from_request(parts, &app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let template_id = *parts .extract::>>() @@ -456,7 +451,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id= extract_user_id_from_request(parts, &app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let offer_id = *parts .extract::>>() @@ -485,7 +480,7 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); - let user_id= extract_user_id_from_request(parts, &app_state).await?; + let user_id = extract_user_id_from_request(parts, &app_state).await?; let offer_id = *parts .extract::>>() diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index 3d350641..5897dc27 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -1,9 +1,9 @@ -use std::ops::DerefMut; use chrono::{DateTime, Utc}; use s3::Bucket; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Transaction}; use sqlx::{Pool, Postgres}; +use std::ops::DerefMut; use uuid::Uuid; use super::{error::ChaosError, storage::Storage}; @@ -63,7 +63,9 @@ pub struct CampaignBannerUpdate { impl Campaign { /// Get a list of all campaigns, both published and unpublished - pub async fn get_all(transaction: &mut Transaction<'_, Postgres>) -> Result, ChaosError> { + pub async fn get_all( + transaction: &mut Transaction<'_, Postgres>, + ) -> Result, ChaosError> { let campaigns = sqlx::query_as!( Campaign, " @@ -78,7 +80,10 @@ impl Campaign { } /// Get a campaign based on it's id - pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { + pub async fn get( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { let campaign = sqlx::query_as!( CampaignDetails, " @@ -97,7 +102,11 @@ impl Campaign { Ok(campaign) } - pub async fn check_slug_availability(organisation_id: i64, slug: String, pool: &Pool) -> Result<(), ChaosError> { + pub async fn check_slug_availability( + organisation_id: i64, + slug: String, + pool: &Pool, + ) -> Result<(), ChaosError> { if !slug.is_ascii() { return Err(ChaosError::BadRequest); } @@ -109,19 +118,23 @@ impl Campaign { organisation_id, slug ) - .fetch_one(pool) - .await? - .exists - .expect("`exists` should always exist in this query result"); + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); if exists { - return Err(ChaosError::BadRequest) + return Err(ChaosError::BadRequest); } Ok(()) } - pub async fn get_by_slugs(organisation_slug: String, campaign_slug: String, transaction: &mut Transaction<'_, Postgres>) -> Result { + pub async fn get_by_slugs( + organisation_slug: String, + campaign_slug: String, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { let campaign = sqlx::query_as!( CampaignDetails, " @@ -135,8 +148,8 @@ impl Campaign { campaign_slug, organisation_slug ) - .fetch_one(transaction.deref_mut()) - .await?; + .fetch_one(transaction.deref_mut()) + .await?; Ok(campaign) } @@ -197,7 +210,10 @@ impl Campaign { } /// Delete a campaign from the database - pub async fn delete(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + pub async fn delete( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { _ = sqlx::query!( " DELETE FROM campaigns WHERE id = $1 RETURNING id diff --git a/backend/server/src/models/email_template.rs b/backend/server/src/models/email_template.rs index cd84130a..dd91cf00 100644 --- a/backend/server/src/models/email_template.rs +++ b/backend/server/src/models/email_template.rs @@ -1,13 +1,10 @@ -use std::collections::HashMap; -use std::ops::DerefMut; +use crate::models::error::ChaosError; use chrono::{DateTime, Local, Utc}; use handlebars::Handlebars; use serde::{Deserialize, Serialize}; -use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres, Transaction}; -use crate::models::application::Application; -use crate::models::campaign::Campaign; -use crate::models::error::ChaosError; +use std::collections::HashMap; +use std::ops::DerefMut; /// Email templates to update applicants /// Supported tags: @@ -25,45 +22,60 @@ pub struct EmailTemplate { } impl EmailTemplate { - pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { + pub async fn get( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { let template = sqlx::query_as!( EmailTemplate, "SELECT * FROM email_templates WHERE id = $1", id ) - .fetch_one(transaction.deref_mut()).await?; + .fetch_one(transaction.deref_mut()) + .await?; Ok(template) } - pub async fn get_all_by_organisation(organisation_id: i64, pool: &Pool) -> Result, ChaosError> { + pub async fn get_all_by_organisation( + organisation_id: i64, + pool: &Pool, + ) -> Result, ChaosError> { let templates = sqlx::query_as!( EmailTemplate, "SELECT * FROM email_templates WHERE organisation_id = $1", organisation_id ) - .fetch_all(pool).await?; + .fetch_all(pool) + .await?; Ok(templates) } - pub async fn update(id: i64, name: String, template: String, pool: &Pool) -> Result<(), ChaosError> { + pub async fn update( + id: i64, + name: String, + template: String, + pool: &Pool, + ) -> Result<(), ChaosError> { let _ = sqlx::query!( " UPDATE email_templates SET name = $2, template = $3 WHERE id = $1 RETURNING id ", - id, name, template + id, + name, + template ) - .fetch_one(pool).await?; + .fetch_one(pool) + .await?; Ok(()) } pub async fn delete(id: i64, pool: &Pool) -> Result<(), ChaosError> { - let _ = sqlx::query!( - "DELETE FROM email_templates WHERE id = $1 RETURNING id", - id - ).fetch_one(pool).await?; + let _ = sqlx::query!("DELETE FROM email_templates WHERE id = $1 RETURNING id", id) + .fetch_one(pool) + .await?; Ok(()) } @@ -87,10 +99,16 @@ impl EmailTemplate { data.insert("role", role); data.insert("organisation_name", organisation_name); data.insert("campaign_name", campaign_name); - data.insert("expiry_date", expiry_date.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()); + data.insert( + "expiry_date", + expiry_date + .with_timezone(&Local) + .format("%d/%m/%Y %H:%M") + .to_string(), + ); let final_string = handlebars.render("template", &data)?; Ok(final_string) } -} \ No newline at end of file +} diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index c8f5f02e..e2d03ca5 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -3,7 +3,9 @@ pub mod app; pub mod application; pub mod auth; pub mod campaign; +pub mod email_template; pub mod error; +pub mod offer; pub mod organisation; pub mod question; pub mod rating; @@ -11,5 +13,3 @@ pub mod role; pub mod storage; pub mod transaction; pub mod user; -pub mod email_template; -pub mod offer; diff --git a/backend/server/src/models/offer.rs b/backend/server/src/models/offer.rs index 52725601..d0201756 100644 --- a/backend/server/src/models/offer.rs +++ b/backend/server/src/models/offer.rs @@ -1,12 +1,10 @@ -use std::ops::DerefMut; +use crate::models::email_template::EmailTemplate; +use crate::models::error::ChaosError; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{Postgres, Transaction}; -use crate::models::application::Application; -use crate::models::campaign::Campaign; -use crate::models::email_template::EmailTemplate; -use crate::models::error::ChaosError; +use std::ops::DerefMut; #[derive(Deserialize)] pub struct Offer { @@ -44,7 +42,7 @@ pub enum OfferStatus { Draft, Sent, Accepted, - Declined + Declined, } #[derive(Deserialize)] @@ -53,7 +51,15 @@ pub struct OfferReply { } impl Offer { - pub async fn create(campaign_id: i64, application_id: i64, email_template_id: i64, role_id: i64, expiry: DateTime, transaction: &mut Transaction<'_, Postgres>, mut snowflake_id_generator: SnowflakeIdGenerator) -> Result { + pub async fn create( + campaign_id: i64, + application_id: i64, + email_template_id: i64, + role_id: i64, + expiry: DateTime, + transaction: &mut Transaction<'_, Postgres>, + mut snowflake_id_generator: SnowflakeIdGenerator, + ) -> Result { let id = snowflake_id_generator.real_time_generate(); let _ = sqlx::query!( @@ -73,7 +79,10 @@ impl Offer { Ok(id) } - pub async fn get(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { + pub async fn get( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { let offer = sqlx::query_as!( OfferDetails, r#" @@ -97,13 +106,16 @@ impl Offer { "#, id ) - .fetch_one(transaction.deref_mut()) - .await?; + .fetch_one(transaction.deref_mut()) + .await?; Ok(offer) } - pub async fn get_by_campaign(campaign_id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result, ChaosError> { + pub async fn get_by_campaign( + campaign_id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result, ChaosError> { let offers = sqlx::query_as!( OfferDetails, r#" @@ -127,13 +139,16 @@ impl Offer { "#, campaign_id ) - .fetch_all(transaction.deref_mut()) - .await?; + .fetch_all(transaction.deref_mut()) + .await?; Ok(offers) } - pub async fn delete(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + pub async fn delete( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { let _ = sqlx::query!("DELETE FROM offers WHERE id = $1 RETURNING id", id) .fetch_one(transaction.deref_mut()) .await?; @@ -141,11 +156,15 @@ impl Offer { Ok(()) } - pub async fn reply(id: i64, accept: bool, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + pub async fn reply( + id: i64, + accept: bool, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { let offer = Offer::get(id, transaction).await?; if Utc::now() > offer.expiry { - return Err(ChaosError::BadRequest) + return Err(ChaosError::BadRequest); } let mut status = OfferStatus::Accepted; @@ -153,24 +172,52 @@ impl Offer { status = OfferStatus::Declined; } - let _ = sqlx::query!("UPDATE offers SET status = $2 WHERE id = $1", id, status as OfferStatus) - .execute(transaction.deref_mut()) - .await?; + let _ = sqlx::query!( + "UPDATE offers SET status = $2 WHERE id = $1", + id, + status as OfferStatus + ) + .execute(transaction.deref_mut()) + .await?; Ok(()) } - pub async fn preview_email(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result { + pub async fn preview_email( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result { let offer = Offer::get(id, transaction).await?; - let email = EmailTemplate::generate_email(offer.user_name, offer.role_name, offer.organisation_name, offer.campaign_name, offer.expiry, offer.email_template_id, transaction).await?; + let email = EmailTemplate::generate_email( + offer.user_name, + offer.role_name, + offer.organisation_name, + offer.campaign_name, + offer.expiry, + offer.email_template_id, + transaction, + ) + .await?; Ok(email) } - pub async fn send_offer(id: i64, transaction: &mut Transaction<'_, Postgres>) -> Result<(), ChaosError> { + pub async fn send_offer( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { let offer = Offer::get(id, transaction).await?; - let email = EmailTemplate::generate_email(offer.user_name, offer.role_name, offer.organisation_name, offer.campaign_name, offer.expiry, offer.email_template_id, transaction).await?; + let email = EmailTemplate::generate_email( + offer.user_name, + offer.role_name, + offer.organisation_name, + offer.campaign_name, + offer.expiry, + offer.email_template_id, + transaction, + ) + .await?; // TODO: Send email e.g. send_email(offer.user_email, email).await?; Ok(()) } -} \ No newline at end of file +} diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 890a02cf..94824c37 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -68,7 +68,7 @@ pub struct AdminToRemove { #[derive(Deserialize)] pub struct SlugCheck { - pub slug: String + pub slug: String, } impl Organisation { @@ -112,7 +112,10 @@ impl Organisation { Ok(()) } - pub async fn check_slug_availability(slug: String, pool: &Pool) -> Result<(), ChaosError> { + pub async fn check_slug_availability( + slug: String, + pool: &Pool, + ) -> Result<(), ChaosError> { if !slug.is_ascii() { return Err(ChaosError::BadRequest); } @@ -123,13 +126,13 @@ impl Organisation { ", slug ) - .fetch_one(pool) - .await? - .exists - .expect("`exists` should always exist in this query result"); + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); if exists { - return Err(ChaosError::BadRequest) + return Err(ChaosError::BadRequest); } Ok(()) @@ -151,7 +154,10 @@ impl Organisation { Ok(organisation) } - pub async fn get_by_slug(slug: String, pool: &Pool) -> Result { + pub async fn get_by_slug( + slug: String, + pool: &Pool, + ) -> Result { let organisation = sqlx::query_as!( OrganisationDetails, " @@ -161,8 +167,8 @@ impl Organisation { ", slug ) - .fetch_one(pool) - .await?; + .fetch_one(pool) + .await?; Ok(organisation) } @@ -430,7 +436,13 @@ impl Organisation { Ok(()) } - pub async fn create_email_template(organisation_id: i64, name: String, template: String, pool: &Pool, mut snowflake_generator: SnowflakeIdGenerator,) -> Result { + pub async fn create_email_template( + organisation_id: i64, + name: String, + template: String, + pool: &Pool, + mut snowflake_generator: SnowflakeIdGenerator, + ) -> Result { let id = snowflake_generator.generate(); let _ = sqlx::query!( @@ -438,11 +450,13 @@ impl Organisation { INSERT INTO email_templates (id, organisation_id, name, template) VALUES ($1, $2, $3, $4) ", - id, organisation_id, - name, template + id, + organisation_id, + name, + template ) - .execute(pool) - .await?; + .execute(pool) + .await?; Ok(id) } diff --git a/backend/server/src/models/role.rs b/backend/server/src/models/role.rs index 43013c93..ec07082e 100644 --- a/backend/server/src/models/role.rs +++ b/backend/server/src/models/role.rs @@ -1,9 +1,9 @@ -use std::ops::DerefMut; use crate::models::error::ChaosError; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{FromRow, Pool, Postgres, Transaction}; +use std::ops::DerefMut; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Role { diff --git a/backend/server/src/service/auth.rs b/backend/server/src/service/auth.rs index 4d4ee154..68b9302f 100644 --- a/backend/server/src/service/auth.rs +++ b/backend/server/src/service/auth.rs @@ -1,14 +1,13 @@ -use axum::extract::FromRef; +use crate::models::app::AppState; +use crate::models::error::ChaosError; +use crate::models::user::UserRole; +use crate::service::jwt::decode_auth_token; use axum::http::request::Parts; use axum::RequestPartsExt; use axum_extra::headers::Cookie; use axum_extra::TypedHeader; -use crate::models::user::UserRole; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; -use crate::models::app::AppState; -use crate::models::error::ChaosError; -use crate::service::jwt::decode_auth_token; /// Checks if a user exists in DB based on given email address. If so, their user_id is returned. /// Otherwise, a new user is created in the DB, and the new id is returned. @@ -53,7 +52,9 @@ pub async fn assert_is_super_user(user_id: i64, pool: &Pool) -> Result UserRole::SuperUser as UserRole ) .fetch_one(pool) - .await?.exists.expect("`exists` should always exist in this query result"); + .await? + .exists + .expect("`exists` should always exist in this query result"); if !is_super_user { return Err(ChaosError::Unauthorized); @@ -62,7 +63,10 @@ pub async fn assert_is_super_user(user_id: i64, pool: &Pool) -> Result Ok(()) } -pub async fn extract_user_id_from_request(parts: &mut Parts, state: &AppState) -> Result { +pub async fn extract_user_id_from_request( + parts: &mut Parts, + state: &AppState, +) -> Result { let decoding_key = &state.decoding_key; let jwt_validator = &state.jwt_validator; let TypedHeader(cookies) = parts @@ -76,4 +80,4 @@ pub async fn extract_user_id_from_request(parts: &mut Parts, state: &AppState) - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; Ok(claims.sub) -} \ No newline at end of file +} diff --git a/backend/server/src/service/email_template.rs b/backend/server/src/service/email_template.rs index c6ad5d31..3ef19e2a 100644 --- a/backend/server/src/service/email_template.rs +++ b/backend/server/src/service/email_template.rs @@ -17,10 +17,10 @@ pub async fn user_is_email_template_admin( template_id, user_id ) - .fetch_one(pool) - .await? - .exists - .expect("`exists` should always exist in this query result"); + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); if !is_admin { return Err(ChaosError::Unauthorized); diff --git a/backend/server/src/service/mod.rs b/backend/server/src/service/mod.rs index 53f76596..e4ad6769 100644 --- a/backend/server/src/service/mod.rs +++ b/backend/server/src/service/mod.rs @@ -2,11 +2,11 @@ pub mod answer; pub mod application; pub mod auth; pub mod campaign; +pub mod email_template; pub mod jwt; pub mod oauth2; +pub mod offer; pub mod organisation; pub mod question; pub mod rating; pub mod role; -pub mod email_template; -pub mod offer; \ No newline at end of file diff --git a/backend/server/src/service/offer.rs b/backend/server/src/service/offer.rs index 1a639441..f044ea7b 100644 --- a/backend/server/src/service/offer.rs +++ b/backend/server/src/service/offer.rs @@ -1,6 +1,6 @@ use crate::models::error::ChaosError; -use sqlx::{Pool, Postgres}; use crate::models::offer::Offer; +use sqlx::{Pool, Postgres}; pub async fn assert_user_is_offer_admin( user_id: i64, @@ -19,10 +19,10 @@ pub async fn assert_user_is_offer_admin( offer_id, user_id ) - .fetch_one(pool) - .await? - .exists - .expect("`exists` should always exist in this query result"); + .fetch_one(pool) + .await? + .exists + .expect("`exists` should always exist in this query result"); if !is_admin { return Err(ChaosError::Unauthorized); @@ -40,8 +40,8 @@ pub async fn assert_user_is_offer_recipient( let offer = Offer::get(offer_id, tx).await?; if offer.user_id != user_id { - return Err(ChaosError::Unauthorized) + return Err(ChaosError::Unauthorized); } Ok(()) -} \ No newline at end of file +}