Skip to content

Commit

Permalink
offer CRUD
Browse files Browse the repository at this point in the history
  • Loading branch information
KavikaPalletenne committed Nov 26, 2024
1 parent 534df8d commit 9d346e0
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 28 deletions.
25 changes: 25 additions & 0 deletions backend/server/src/handler/campaign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -126,4 +127,28 @@ impl CampaignHandler {
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(applications)))
}

pub async fn create_offer(
Path(id): Path<i64>,
State(state): State<AppState>,
_admin: CampaignAdmin,
mut transaction: DBTransaction<'_>,
Json(data): Json<Offer>
) -> Result<impl IntoResponse, ChaosError> {
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<i64>,
_user: CampaignAdmin
) -> Result<impl IntoResponse, ChaosError> {
let offers = Offer::get_by_campaign(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, Json(offers)))
}
}
1 change: 1 addition & 0 deletions backend/server/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pub mod rating;
pub mod role;
pub mod user;
pub mod email_template;
pub mod offer;
67 changes: 67 additions & 0 deletions backend/server/src/handler/offer.rs
Original file line number Diff line number Diff line change
@@ -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<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
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<i64>,
_user: OfferAdmin
) -> Result<impl IntoResponse, ChaosError> {
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<i64>,
_user: OfferRecipient,
Json(reply): Json<OfferReply>,
) -> Result<impl IntoResponse, ChaosError> {
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<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
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<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
Offer::send_offer(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, "Successfully sent offer"))
}
}
21 changes: 21 additions & 0 deletions backend/server/src/models/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -205,6 +206,14 @@ pub async fn app() -> Result<Router, ChaosError> {
"/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),
Expand Down Expand Up @@ -239,5 +248,17 @@ pub async fn app() -> Result<Router, ChaosError> {
.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))
}
62 changes: 61 additions & 1 deletion backend/server/src/models/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -431,11 +433,69 @@ where
.extract::<Path<HashMap<String, i64>>>()
.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?;

Ok(EmailTemplateAdmin { user_id })
}
}

pub struct OfferAdmin {
pub user_id: i64,
}

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

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

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

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

let offer_id = *parts
.extract::<Path<HashMap<String, i64>>>()
.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 })
}
}
57 changes: 31 additions & 26 deletions backend/server/src/models/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Utc>,
status: OfferStatus,
created_at: DateTime<Utc>,
pub id: i64,
pub campaign_id: i64,
pub application_id: i64,
pub email_template_id: i64,
pub role_id: i64,
pub expiry: DateTime<Utc>,
pub status: OfferStatus,
pub created_at: DateTime<Utc>,
}

#[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<Utc>,
status: OfferStatus,
created_at: DateTime<Utc>,
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<Utc>,
pub status: OfferStatus,
pub created_at: DateTime<Utc>,
}

#[derive(Deserialize, Serialize, sqlx::Type, Clone, Debug)]
Expand All @@ -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<Utc>, 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<Utc>, transaction: &mut Transaction<'_, Postgres>, mut snowflake_id_generator: SnowflakeIdGenerator) -> Result<i64, ChaosError> {
let id = snowflake_id_generator.real_time_generate();

let _ = sqlx::query!(
Expand All @@ -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<OfferDetails, ChaosError> {
Expand Down Expand Up @@ -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)
}

Expand Down
3 changes: 2 additions & 1 deletion backend/server/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub mod organisation;
pub mod question;
pub mod rating;
pub mod role;
pub mod email_template;
pub mod email_template;
pub mod offer;
47 changes: 47 additions & 0 deletions backend/server/src/service/offer.rs
Original file line number Diff line number Diff line change
@@ -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<Postgres>,
) -> 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<Postgres>,
) -> 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(())
}

0 comments on commit 9d346e0

Please sign in to comment.