Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chaos 463 campaigns crud #486

Merged
merged 17 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/migrations/20240406024211_create_organisations.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CREATE TABLE organisations (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
logo TEXT,
logo UUID,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
);
Expand Down
2 changes: 1 addition & 1 deletion backend/migrations/20240406025537_create_campaigns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ CREATE TABLE campaigns (
id BIGINT PRIMARY KEY,
organisation_id BIGINT NOT NULL,
name TEXT NOT NULL,
cover_image TEXT,
cover_image UUID,
description TEXT,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
Expand Down
57 changes: 57 additions & 0 deletions backend/server/src/handler/campaign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::models;
use crate::models::app::AppState;
use crate::models::auth::AuthUser;
use crate::models::auth::CampaignAdmin;
use crate::models::campaign::Campaign;
use crate::models::error::ChaosError;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;

pub struct CampaignHandler;
impl CampaignHandler {
pub async fn get(
State(state): State<AppState>,
Path(id): Path<i64>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let campaign = Campaign::get(id, &state.db).await?;
Ok((StatusCode::OK, Json(campaign)))
}

pub async fn get_all(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let campaigns = Campaign::get_all(&state.db).await?;
Ok((StatusCode::OK, Json(campaigns)))
}

pub async fn update(
State(state): State<AppState>,
Path(id): Path<i64>,
_admin: CampaignAdmin,
Json(request_body): Json<models::campaign::CampaignUpdate>,
) -> Result<impl IntoResponse, ChaosError> {
Campaign::update(id, request_body, &state.db).await?;
Ok((StatusCode::OK, "Successfully updated campaign"))
}

pub async fn update_banner(
State(state): State<AppState>,
Path(id): Path<i64>,
_admin: CampaignAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let banner_url = Campaign::update_banner(id, &state.db, &state.storage_bucket).await?;
Ok((StatusCode::OK, Json(banner_url)))
}

pub async fn delete(
State(state): State<AppState>,
Path(id): Path<i64>,
_admin: CampaignAdmin,
) -> Result<impl IntoResponse, ChaosError> {
Campaign::delete(id, &state.db).await?;
Ok((StatusCode::OK, "Successfully deleted campaign"))
}
}
1 change: 1 addition & 0 deletions backend/server/src/handler/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod auth;
pub mod campaign;
pub mod organisation;
5 changes: 2 additions & 3 deletions backend/server/src/handler/organisation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::models::auth::{AuthUser, OrganisationAdmin};
use crate::models::error::ChaosError;
use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation};
use crate::models::transaction::DBTransaction;
use crate::service;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
Expand Down Expand Up @@ -68,7 +67,6 @@ impl OrganisationHandler {
}

pub async fn update_admins(
State(state): State<AppState>,
Path(id): Path<i64>,
_super_user: SuperUser,
mut transaction: DBTransaction<'_>,
Expand All @@ -81,7 +79,6 @@ impl OrganisationHandler {
}

pub async fn update_members(
State(state): State<AppState>,
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_admin: OrganisationAdmin,
Expand Down Expand Up @@ -141,11 +138,13 @@ impl OrganisationHandler {
}

pub async fn create_campaign(
Path(id): Path<i64>,
State(mut state): State<AppState>,
_admin: OrganisationAdmin,
Json(request_body): Json<models::campaign::Campaign>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::create_campaign(
id,
request_body.name,
request_body.description,
request_body.starts_at,
Expand Down
14 changes: 13 additions & 1 deletion backend/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::handler::auth::google_callback;
use crate::handler::campaign::CampaignHandler;
use crate::handler::organisation::OrganisationHandler;
use crate::models::storage::Storage;
use anyhow::Result;
use axum::routing::{get, patch, post, put};
use axum::routing::{get, patch, post};
use axum::Router;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
use models::app::AppState;
Expand Down Expand Up @@ -89,6 +90,17 @@ async fn main() -> Result<()> {
.put(OrganisationHandler::update_admins)
.delete(OrganisationHandler::remove_admin),
)
.route(
"/api/v1/campaign/:id",
get(CampaignHandler::get)
.put(CampaignHandler::update)
.delete(CampaignHandler::delete),
)
.route("/api/v1/campaign", get(CampaignHandler::get_all))
.route(
"/api/v1/campaign/:id/banner",
patch(CampaignHandler::update_banner),
)
.with_state(state);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
Expand Down
45 changes: 43 additions & 2 deletions backend/server/src/models/auth.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::models::app::AppState;
use crate::models::error::ChaosError;
use crate::service::auth::is_super_user;
use crate::service::campaign::user_is_campaign_admin;
use crate::service::jwt::decode_auth_token;
use crate::service::organisation::user_is_admin;
use crate::service::organisation::user_is_organisation_admin;
use axum::extract::{FromRef, FromRequestParts, Path};
use axum::http::request::Parts;
use axum::response::{IntoResponse, Redirect, Response};
Expand Down Expand Up @@ -139,8 +140,48 @@ where
.await
.map_err(|_| ChaosError::BadRequest)?;

user_is_admin(user_id, organisation_id, pool).await?;
user_is_organisation_admin(user_id, organisation_id, pool).await?;

Ok(OrganisationAdmin { user_id })
}
}

pub struct CampaignAdmin {
pub user_id: i64,
}

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

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let decoding_key = &app_state.decoding_key;
let jwt_validator = &app_state.jwt_validator;
let TypedHeader(cookies) = parts
.extract::<TypedHeader<Cookie>>()
.await
.map_err(|_| ChaosError::NotLoggedIn)?;

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

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

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

let Path(campaign_id) = parts
.extract::<Path<i64>>()
.await
.map_err(|_| ChaosError::BadRequest)?;

user_is_campaign_admin(user_id, campaign_id, pool).await?;

Ok(CampaignAdmin { user_id })
}
}
152 changes: 150 additions & 2 deletions backend/server/src/models/campaign.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,160 @@
use chrono::{DateTime, Utc};
use s3::Bucket;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use sqlx::{Pool, Postgres};
use uuid::Uuid;

#[derive(Deserialize, Serialize, Clone, Debug)]
use super::{error::ChaosError, storage::Storage};

#[derive(Deserialize, Serialize, Clone, FromRow, Debug)]
pub struct Campaign {
pub id: i64,
pub name: String,
pub organisation_id: i64,
pub organisation_name: String,
pub cover_image: Option<Uuid>,
pub description: Option<String>,
pub cover_image: Option<String>,
pub starts_at: DateTime<Utc>,
pub ends_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}

#[derive(Deserialize, Serialize, Clone, FromRow, Debug)]
pub struct CampaignDetails {
pub id: i64,
pub name: String,
pub organisation_id: i64,
pub organisation_name: String,
pub cover_image: Option<Uuid>,
pub description: Option<String>,
pub starts_at: DateTime<Utc>,
pub ends_at: DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Clone, FromRow, Debug)]
pub struct OrganisationCampaign {
pub id: i64,
pub name: String,
pub cover_image: Option<Uuid>,
pub description: Option<String>,
pub starts_at: DateTime<Utc>,
pub ends_at: DateTime<Utc>,
}

#[derive(Deserialize, Serialize, Clone, FromRow, Debug)]
pub struct CampaignUpdate {
pub name: String,
pub description: String,
pub starts_at: DateTime<Utc>,
pub ends_at: DateTime<Utc>,
}

#[derive(Serialize)]
pub struct CampaignBannerUpdate {
pub upload_url: String,
}

impl Campaign {
/// Get a list of all campaigns, both published and unpublished
pub async fn get_all(pool: &Pool<Postgres>) -> Result<Vec<Campaign>, ChaosError> {
let campaigns = sqlx::query_as!(
Campaign,
"
SELECT c.*, o.name as organisation_name FROM campaigns c
JOIN organisations o on c.organisation_id = o.id
"
)
.fetch_all(pool)
.await?;

Ok(campaigns)
}

/// Get a campaign based on it's id
pub async fn get(id: i64, pool: &Pool<Postgres>) -> Result<CampaignDetails, ChaosError> {
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
FROM campaigns c
JOIN organisations o on c.organisation_id = o.id
WHERE c.id = $1
",
id
)
.fetch_one(pool)
.await?;

Ok(campaign)
}

/// Update a campaign for all fields that are not None
pub async fn update(
id: i64,
update: CampaignUpdate,
pool: &Pool<Postgres>,
) -> Result<(), ChaosError> {
sqlx::query!(
"
UPDATE campaigns
SET name = $1, description = $2, starts_at = $3, ends_at = $4
WHERE id = $5
",
update.name,
update.description,
update.starts_at,
update.ends_at,
id
)
.execute(pool)
.await?;

Ok(())
}

/// Update a campaign banner
/// Returns the updated campaign
pub async fn update_banner(
id: i64,
pool: &Pool<Postgres>,
storage_bucket: &Bucket,
) -> Result<CampaignBannerUpdate, ChaosError> {
let dt = Utc::now();
let image_id = Uuid::new_v4();
let current_time = dt;

sqlx::query!(
"
UPDATE campaigns
SET cover_image = $1, updated_at = $2
WHERE id = $3
",
image_id,
current_time,
id
)
.execute(pool)
.await?;

let upload_url =
Storage::generate_put_url(format!("/banner/{id}/{image_id}"), storage_bucket).await?;

Ok(CampaignBannerUpdate { upload_url })
}

/// Delete a campaign from the database
pub async fn delete(id: i64, pool: &Pool<Postgres>) -> Result<(), ChaosError> {
sqlx::query!(
"
DELETE FROM campaigns WHERE id = $1
",
id
)
.execute(pool)
.await?;

Ok(())
}
}
Loading
Loading