From 9a36bfa35cc9eecf22d3869d496f200b41761c39 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 30 Mar 2024 14:59:45 +1100 Subject: [PATCH 01/36] feat: Initial draft TODO: TESTING + more clarification --- backend/server/src/handler/mod.rs | 1 + backend/server/src/handler/organisation.rs | 65 +++++++++++ backend/server/src/models/mod.rs | 1 + backend/server/src/models/organisation.rs | 25 +++++ backend/server/src/service/mod.rs | 1 + backend/server/src/service/organisation.rs | 120 +++++++++++++++++++++ 6 files changed, 213 insertions(+) create mode 100644 backend/server/src/handler/organisation.rs create mode 100644 backend/server/src/models/organisation.rs create mode 100644 backend/server/src/service/organisation.rs diff --git a/backend/server/src/handler/mod.rs b/backend/server/src/handler/mod.rs index 0e4a05d5..af037dc1 100644 --- a/backend/server/src/handler/mod.rs +++ b/backend/server/src/handler/mod.rs @@ -1 +1,2 @@ pub mod auth; +pub mod organisation; \ No newline at end of file diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs new file mode 100644 index 00000000..3d2a2e7f --- /dev/null +++ b/backend/server/src/handler/organisation.rs @@ -0,0 +1,65 @@ +use crate::models::app::AppState; +use crate::models::auth::AuthUser; +use axum::http::StatusCode; +use crate::service; +use axum::extract::{Path, State}; +use axum::Json; +use axum::response::IntoResponse; +use crate::models; + +pub async fn get_organisation_name( + State(state): State, + Path(organisation_id): Path, + _user: AuthUser +) -> Result { + match service::organisation::get_organisation_name(organisation_id, state.db).await { + Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} + +pub async fn get_organisation( + State(state): State, + Path(organisation_id): Path, + _user: AuthUser +) -> Result { + match service::organisation::get_organisation(organisation_id, state.db).await { + Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} + +pub async fn update_organisation( + State(state): State, + Path(organisation_id): Path, + _user: SuperUser, + Json(request_body): Json +) -> Result { + match service::organisation::update_organisation(organisation_id, request_body.name, request_body.logo, state.db).await { + Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} + +pub async fn delete_organisation( + State(state): State, + Path(organisation_id): Path, + _user: SuperUser, +) -> Result { + match service::organisation::update_organisation(organisation_id, state.db).await { + Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} + +pub async fn create_organisation( + State(state): State, + Path(organisation_id): Path, + _user: SuperUser, + Json(request_body): Json +) -> Result { + match service::organisation::create_organisation(organisation_id, request_body.name, request_body.logo, state.db).await { + Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} \ No newline at end of file diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index 2164071c..2879f9cd 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -1,3 +1,4 @@ pub mod app; pub mod auth; pub mod user; +pub mod organisation; \ No newline at end of file diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs new file mode 100644 index 00000000..0e2f0f29 --- /dev/null +++ b/backend/server/src/models/organisation.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, TimeZone, Utc}; + +#[derive(Deserialize, Serialize, sqlx::Type, Clone)] +#[sqlx(type_name = "Organisation", rename_all = "PascalCase")] +pub struct Organisation { + pub id: i64, + pub name: String, + pub logo: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + #[serde(skip_serializing)] + #[serde(skip_deserializing)] + pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done + #[serde(skip_serializing)] + #[serde(skip_deserializing)] + pub organisation_admins: Vec +} + +#[derive(Deserialize, Serialize, sqlx::Type, Clone)] +#[sqlx(type_name = "OrganisationAdmins", rename_all = "PascalCase")] +pub struct OrganisationAdmins { + pub organisation_id: i64, // References id in Organisation table + pub user_id: i64 // References id in User table +} diff --git a/backend/server/src/service/mod.rs b/backend/server/src/service/mod.rs index 6e59a7bb..df48d803 100644 --- a/backend/server/src/service/mod.rs +++ b/backend/server/src/service/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod jwt; pub mod oauth2; +pub mod organisation; \ No newline at end of file diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs new file mode 100644 index 00000000..0fb50d5d --- /dev/null +++ b/backend/server/src/service/organisation.rs @@ -0,0 +1,120 @@ +use anyhow::{bail, Result}; +use sqlx::{Pool, Postgres}; +use chrono::Utc; + +pub async fn get_organisation_name(id: i64, pool: Pool) -> Result { + let organisation_name = sqlx::query!("SELECT name FROM organisations WHERE id = $1", id) + .fetch_optional(&pool) + .await?; + + if let Some(result) = organisation_name { + Ok(result.name) + } + + bail!("error"); +} + +pub async fn get_organisation(id: i64, email: String, pool: Pool) -> Result { + let org_name = sqlx::query!("SELECT * FROM organisations WHERE id = $1", id) + .fetch_optional(&pool) + .await?; + + if let Some(result) = org_name { + Ok(result.name) + } + + bail!("error"); +} + +pub async fn update_organisation(id: i64, name: Option, logo: Option, pool: Pool) -> Result { + let mut query = String::from("UPDATE organisations SET "); + let mut name_exists = false; + + if let Some(name) = name { + query.push_str("name = $2"); + name_exists = true; + } + + if let Some(logo) = logo { + if name_exists { + query.push_str(", "); + } + query.push_str("logo = $3"); + } + + query.push_str(", updated_at = $"); + query.push_str(" WHERE id = $1"); + + // This is under the assumption that Postgres will updated updated_at itself? + let rows_affected = sqlx::query!( + &query, + id, + name, + logo + Utc::now() + ) + .execute(&pool) + .await?; + + if let Some(rows_affected) = rows_affected { + Ok(rows_affected) + } + + bail!("error: failed to update organisation") +} + +pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: &sqlx::Pool) -> Result { + let query = " + DELETE FROM organisations + WHERE id = $1 + AND EXISTS ( + SELECT 1 FROM organisation_admins + WHERE organisation_id = $1 AND user_id = $2 + )"; + + let deleted_organisation = sqlx::query!( + &query, + organisation_id, + admin_id, + Utc::now() + ) + .execute(&pool) + .await?; + + if let Some(deleted_organisation) = deleted_organisation { + Ok(deleted_organisation) + } + + bail!("error: failed to delete organisation") +} + +pub async fn create_organisation(name: String, logo: Option) -> Result { + let query = if logo.is_some() { + "INSERT INTO organisations (name, logo) VALUES ($1, $2)" + } else { + "INSERT INTO organisations (name) VALUES ($1)" + }; + + let new_organisation = if let Some(logo) = logo { + sqlx::query!( + &query, + name, + logo + ) + .execute(&pool) + .await? + } else { + sqlx::query!( + &query, + name, + ) + .execute(&pool) + .await? + }; + + if let Some(new_organisation) = new_organisation { + Ok(new_organisation) + } + + bail!("error: failed to create organisation") +} \ No newline at end of file From dd3c390c5e5b5923312366e88545df8fac94ef9e Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 30 Mar 2024 15:03:08 +1100 Subject: [PATCH 02/36] rearranged import --- backend/server/src/handler/organisation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 3d2a2e7f..4bf37cd2 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,11 +1,11 @@ use crate::models::app::AppState; use crate::models::auth::AuthUser; use axum::http::StatusCode; +use crate::models; use crate::service; use axum::extract::{Path, State}; use axum::Json; use axum::response::IntoResponse; -use crate::models; pub async fn get_organisation_name( State(state): State, From 43161580b176b9aaf11eb490f4a240edcfa6dc87 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 30 Mar 2024 15:51:08 +1100 Subject: [PATCH 03/36] clear errors --- backend/server/src/handler/organisation.rs | 8 +-- backend/server/src/models/organisation.rs | 14 ++-- backend/server/src/service/organisation.rs | 82 +++++++++------------- 3 files changed, 42 insertions(+), 62 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 4bf37cd2..f2751b2a 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,5 +1,6 @@ use crate::models::app::AppState; use crate::models::auth::AuthUser; +use crate::models::auth::SuperUser; use axum::http::StatusCode; use crate::models; use crate::service; @@ -35,7 +36,7 @@ pub async fn update_organisation( _user: SuperUser, Json(request_body): Json ) -> Result { - match service::organisation::update_organisation(organisation_id, request_body.name, request_body.logo, state.db).await { + match service::organisation::update_organisation(organisation_id, Some(request_body.name), request_body.logo, state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -46,7 +47,7 @@ pub async fn delete_organisation( Path(organisation_id): Path, _user: SuperUser, ) -> Result { - match service::organisation::update_organisation(organisation_id, state.db).await { + match service::organisation::delete_organisation(organisation_id, _user.user_id, state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -54,11 +55,10 @@ pub async fn delete_organisation( pub async fn create_organisation( State(state): State, - Path(organisation_id): Path, _user: SuperUser, Json(request_body): Json ) -> Result { - match service::organisation::create_organisation(organisation_id, request_body.name, request_body.logo, state.db).await { + match service::organisation::create_organisation(request_body.name, request_body.logo, state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 0e2f0f29..431f8a47 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -1,24 +1,22 @@ use serde::{Deserialize, Serialize}; -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, Utc}; -#[derive(Deserialize, Serialize, sqlx::Type, Clone)] -#[sqlx(type_name = "Organisation", rename_all = "PascalCase")] +#[derive(Deserialize, Serialize, Clone)] pub struct Organisation { pub id: i64, pub name: String, pub logo: Option, pub created_at: DateTime, pub updated_at: DateTime, - #[serde(skip_serializing)] - #[serde(skip_deserializing)] - pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done + // #[serde(skip_serializing)] + // #[serde(skip_deserializing)] + // pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done #[serde(skip_serializing)] #[serde(skip_deserializing)] pub organisation_admins: Vec } -#[derive(Deserialize, Serialize, sqlx::Type, Clone)] -#[sqlx(type_name = "OrganisationAdmins", rename_all = "PascalCase")] +#[derive(Deserialize, Serialize, Clone)] pub struct OrganisationAdmins { pub organisation_id: i64, // References id in Organisation table pub user_id: i64 // References id in User table diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index 0fb50d5d..5eab0941 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -8,19 +8,19 @@ pub async fn get_organisation_name(id: i64, pool: Pool) -> Result) -> Result { +pub async fn get_organisation(id: i64, pool: Pool) -> Result { let org_name = sqlx::query!("SELECT * FROM organisations WHERE id = $1", id) .fetch_optional(&pool) .await?; if let Some(result) = org_name { - Ok(result.name) + return Ok(result.name); } bail!("error"); @@ -30,12 +30,12 @@ pub async fn update_organisation(id: i64, name: Option, logo: Option, logo: Option return Ok("done".to_string()), + Err(_) => bail!("error: Failed to update organisation") } - - bail!("error: failed to update organisation") } -pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: &sqlx::Pool) -> Result { - let query = " +pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { + let deleted_organisation = sqlx::query!(" DELETE FROM organisations WHERE id = $1 AND EXISTS ( SELECT 1 FROM organisation_admins WHERE organisation_id = $1 AND user_id = $2 - )"; - - let deleted_organisation = sqlx::query!( - &query, + )", organisation_id, - admin_id, - Utc::now() + admin_id ) - .execute(&pool) + .fetch_optional(&pool) .await?; - if let Some(deleted_organisation) = deleted_organisation { - Ok(deleted_organisation) + if let Some(_deleted_organisation) = deleted_organisation { + return Ok("done".to_string()); } bail!("error: failed to delete organisation") } -pub async fn create_organisation(name: String, logo: Option) -> Result { - let query = if logo.is_some() { - "INSERT INTO organisations (name, logo) VALUES ($1, $2)" - } else { - "INSERT INTO organisations (name) VALUES ($1)" - }; - +pub async fn create_organisation(name: String, logo: Option, pool: Pool) -> Result { + // Here I'm assuming postgres will create a unique id let new_organisation = if let Some(logo) = logo { - sqlx::query!( - &query, - name, - logo - ) - .execute(&pool) + sqlx::query!("INSERT INTO organisations (name, logo) VALUES ($1, $2)", name, logo) + .fetch_optional(&pool) .await? } else { - sqlx::query!( - &query, - name, - ) - .execute(&pool) + sqlx::query!("INSERT INTO organisations (name) VALUES ($1)", name) + .fetch_optional(&pool) .await? }; - if let Some(new_organisation) = new_organisation { - Ok(new_organisation) + if let Some(_new_organisation) = new_organisation { + return Ok("done".to_string()); } bail!("error: failed to create organisation") From cd0d8da8cdf446f38506a65d2f831d0bcb801ae6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 31 Mar 2024 21:34:23 +1100 Subject: [PATCH 04/36] post testing --- backend/server/src/handler/organisation.rs | 55 ++++- backend/server/src/main.rs | 14 ++ backend/server/src/models/organisation.rs | 9 +- backend/server/src/service/organisation.rs | 234 +++++++++++++++------ 4 files changed, 228 insertions(+), 84 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index f2751b2a..43ba1e4a 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,19 +1,16 @@ use crate::models::app::AppState; use crate::models::auth::AuthUser; -use crate::models::auth::SuperUser; use axum::http::StatusCode; use crate::models; use crate::service; -use axum::extract::{Path, State}; -use axum::Json; +use axum::extract::{Path, State, Json}; use axum::response::IntoResponse; -pub async fn get_organisation_name( +pub async fn get_organisations( State(state): State, - Path(organisation_id): Path, _user: AuthUser ) -> Result { - match service::organisation::get_organisation_name(organisation_id, state.db).await { + match service::organisation::get_organisations(state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -33,8 +30,8 @@ pub async fn get_organisation( pub async fn update_organisation( State(state): State, Path(organisation_id): Path, - _user: SuperUser, - Json(request_body): Json + _user: AuthUser, + Json(request_body): Json, ) -> Result { match service::organisation::update_organisation(organisation_id, Some(request_body.name), request_body.logo, state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), @@ -45,7 +42,7 @@ pub async fn update_organisation( pub async fn delete_organisation( State(state): State, Path(organisation_id): Path, - _user: SuperUser, + _user: AuthUser ) -> Result { match service::organisation::delete_organisation(organisation_id, _user.user_id, state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), @@ -55,11 +52,47 @@ pub async fn delete_organisation( pub async fn create_organisation( State(state): State, - _user: SuperUser, + _user: AuthUser, Json(request_body): Json ) -> Result { - match service::organisation::create_organisation(request_body.name, request_body.logo, state.db).await { + let mut snowflake_generator = state.snowflake_generator; + let new_organisation_id = snowflake_generator.real_time_generate(); + + match service::organisation::create_organisation(new_organisation_id, request_body.name, request_body.logo, state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } +} + +pub async fn get_organisation_admins( + State(state): State, + Path(organisation_id): Path, + _user: AuthUser +) -> Result { + match service::organisation::get_organisation_admins(organisation_id, state.db).await { + Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} + +pub async fn add_admin_to_organisation( + State(state): State, + Path(organisation_id): Path, + _user: AuthUser +) -> Result { + match service::organisation::add_admin_to_organisation(organisation_id, _user.user_id, state.db).await { + Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} + +pub async fn remove_admin_from_organisation( + State(state): State, + Path(organisation_id): Path, + _user: AuthUser +) -> Result { + match service::organisation::remove_admin_from_organisation(organisation_id, _user.user_id, state.db).await { + Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } } \ No newline at end of file diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index e9d84481..ff81fcc1 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -48,6 +48,20 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) + .route("/api/v1/organisations", + get(handler::organisation::get_organisations) + .post(handler::organisation::create_organisation) + ) + .route("/api/v1/organisations/:organisation_id", + get(handler::organisation::get_organisation) + .patch(handler::organisation::update_organisation) + .delete(handler::organisation::delete_organisation) + ) + .route("/api/v1/organisations/admin/:organisation_id", + get(handler::organisation::get_organisation_admins) + .post(handler::organisation::add_admin_to_organisation) + .delete(handler::organisation::remove_admin_from_organisation) + ) .with_state(state); axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 431f8a47..efa4a8b6 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -1,22 +1,19 @@ use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use chrono::{DateTime, Utc}; -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Organisation { pub id: i64, pub name: String, pub logo: Option, pub created_at: DateTime, pub updated_at: DateTime, - // #[serde(skip_serializing)] - // #[serde(skip_deserializing)] // pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done - #[serde(skip_serializing)] - #[serde(skip_deserializing)] pub organisation_admins: Vec } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct OrganisationAdmins { pub organisation_id: i64, // References id in Organisation table pub user_id: i64 // References id in User table diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index 5eab0941..f5698dbc 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,102 +1,202 @@ use anyhow::{bail, Result}; -use sqlx::{Pool, Postgres}; -use chrono::Utc; +use sqlx::{types::time::{OffsetDateTime, PrimitiveDateTime}, Pool, Postgres}; -pub async fn get_organisation_name(id: i64, pool: Pool) -> Result { - let organisation_name = sqlx::query!("SELECT name FROM organisations WHERE id = $1", id) - .fetch_optional(&pool) +pub async fn get_organisations(pool: Pool) -> Result> { + let organisations = sqlx::query!("SELECT * FROM organisations") + .fetch_all(&pool) .await?; - if let Some(result) = organisation_name { - return Ok(result.name); + let mut name_list: Vec = Vec::new(); + + for organisation in organisations { + name_list.push(organisation.name); } - bail!("error"); + Ok(name_list) } + pub async fn get_organisation(id: i64, pool: Pool) -> Result { - let org_name = sqlx::query!("SELECT * FROM organisations WHERE id = $1", id) - .fetch_optional(&pool) + let org_name = sqlx::query! + ( + " + SELECT * + FROM organisations + WHERE id = $1 + ", + id).fetch_optional(&pool) .await?; if let Some(result) = org_name { return Ok(result.name); } - bail!("error"); + bail!("error: failed to get organisation"); } pub async fn update_organisation(id: i64, name: Option, logo: Option, pool: Pool) -> Result { - let mut query = String::from("UPDATE organisations SET "); - let mut name_exists = false; - - if name.is_some() { - query.push_str("name = $2"); - name_exists = true; - } - - if logo.is_some() { - if name_exists { - query.push_str(", "); - } - query.push_str("logo = $3"); - } - - query.push_str(", updated_at = $"); - query.push_str(" WHERE id = $1"); - - // This is under the assumption that Postgres will updated updated_at itself? - let updated_organisation = sqlx::query(&query) - .bind(id) - .bind(&name) - .bind(&logo) - .bind(Utc::now().to_rfc3339()) - .fetch_one(&pool) - .await; - + let current_time = OffsetDateTime::now_utc(); + let primitive_time = PrimitiveDateTime::new(current_time.date(), current_time.time()); + let updated_organisation = sqlx::query! + ( + " + UPDATE organisations + SET name = $2, logo = $3, updated_at = $4 + WHERE id = $1 + RETURNING * + ", + id, + name, + logo, + primitive_time + ).fetch_one(&pool) + .await; match updated_organisation { - Ok(_) => return Ok("done".to_string()), + Ok(_) => { + println!("{:?}", updated_organisation); + return Ok("done".to_string()) + }, Err(_) => bail!("error: Failed to update organisation") } } pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { - let deleted_organisation = sqlx::query!(" - DELETE FROM organisations - WHERE id = $1 - AND EXISTS ( - SELECT 1 FROM organisation_admins - WHERE organisation_id = $1 AND user_id = $2 - )", + // Delete all organisation_admins with the corresponding organisation_id + let deleted_users = sqlx::query! + ( + " + WITH user_exists AS ( + SELECT 1 AS exists_flag + FROM organisation_admins + WHERE organisation_id = $1 + AND user_id = $2 + LIMIT 1 + ) + DELETE FROM organisation_admins + WHERE organisation_id = $1 + AND EXISTS ( + SELECT 1 + FROM user_exists + ) + RETURNING * + ", organisation_id, admin_id - ) - .fetch_optional(&pool) - .await?; + ).fetch_one(&pool) + .await; + + match deleted_users { + Ok(_) => { + println!("{:?}", deleted_users); + }, + Err(_) => bail!("error: user is not an admin of the organisation") + } + + + // Delete organisation + let deleted_organisation = sqlx::query! + ( + " + DELETE FROM organisations WHERE id = $1 + RETURNING * + ", + organisation_id + ).fetch_one(&pool) + .await; + + match deleted_organisation { + Ok(_) => { + println!("{:?}", deleted_organisation); + Ok("Delete organisation and all the admins from database".to_string()) + }, + Err(_) => bail!("error: user is not an admin of the organisation") +} - if let Some(_deleted_organisation) = deleted_organisation { - return Ok("done".to_string()); +} + +pub async fn create_organisation(id: i64, name: String, logo: Option, pool: Pool) -> Result { + let new_organisation = sqlx::query! + ( + " + INSERT INTO organisations (id, name, logo) + VALUES ($1, $2, $3) + RETURNING * + ", + id, + name, + logo + ).fetch_one(&pool).await; + + match new_organisation { + Ok(_) => { + println!("{:?}", new_organisation); + Ok("Created new organisation in database".to_string()) + }, + Err(_) => bail!("error: failed to create organisation") + } +} + +pub async fn get_organisation_admins(organisation_id: i64, pool: Pool) -> Result> { + let admin_list = sqlx::query! + ( + " + SELECT user_id + FROM organisation_admins + WHERE organisation_id = $1 + " + , organisation_id + ).fetch_all(&pool).await?; + + let mut admin_id_list: Vec = Vec::new(); + + for admin in admin_list { + admin_id_list.push(admin.user_id); } - bail!("error: failed to delete organisation") + Ok(admin_id_list) } -pub async fn create_organisation(name: String, logo: Option, pool: Pool) -> Result { - // Here I'm assuming postgres will create a unique id - let new_organisation = if let Some(logo) = logo { - sqlx::query!("INSERT INTO organisations (name, logo) VALUES ($1, $2)", name, logo) - .fetch_optional(&pool) - .await? - } else { - sqlx::query!("INSERT INTO organisations (name) VALUES ($1)", name) - .fetch_optional(&pool) - .await? - }; - - if let Some(_new_organisation) = new_organisation { - return Ok("done".to_string()); +pub async fn add_admin_to_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { + let new_organisation_admin = sqlx::query! + ( + " + INSERT INTO organisation_admins + (organisation_id, user_id) + VALUES ($1, $2) + RETURNING * + ", + organisation_id, + admin_id + ).fetch_one(&pool).await; + + match new_organisation_admin { + Ok(_) => { + println!("{:?}", new_organisation_admin); + Ok("Created new organisation admin in database".to_string()) + }, + Err(_) => bail!("error: failed to add admin to organisation") } +} - bail!("error: failed to create organisation") +pub async fn remove_admin_from_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { + let new_organisation_admin = sqlx::query! + ( + " + DELETE FROM organisation_admins + WHERE organisation_id = $1 + AND user_id = $2 + RETURNING * + ", + organisation_id, + admin_id + ).fetch_one(&pool).await; + + match new_organisation_admin { + Ok(_) => { + println!("{:?}", new_organisation_admin); + Ok("Deleted organisation admin in database".to_string()) + }, + Err(_) => bail!("error: failed to remove admin from organisation") + } } \ No newline at end of file From 0740566c98c571b54bcc15fe6393e8875a7c076a Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 11:50:09 +1000 Subject: [PATCH 05/36] feat(db): add indexes for foreign key reference columns --- .../20240406024211_create_organisations.sql | 2 ++ .../migrations/20240406025537_create_campaigns.sql | 2 ++ .../migrations/20240406031400_create_questions.sql | 2 ++ .../20240406031915_create_applications.sql | 14 ++++++++++++++ 4 files changed, 20 insertions(+) diff --git a/backend/migrations/20240406024211_create_organisations.sql b/backend/migrations/20240406024211_create_organisations.sql index 6ba4bbca..48f41aee 100644 --- a/backend/migrations/20240406024211_create_organisations.sql +++ b/backend/migrations/20240406024211_create_organisations.sql @@ -16,3 +16,5 @@ CREATE TABLE organisation_admins ( ON DELETE CASCADE ON UPDATE CASCADE ) + +CREATE INDEX IDX_organisation_admins_organisation on organisation_admins (organisation_id); diff --git a/backend/migrations/20240406025537_create_campaigns.sql b/backend/migrations/20240406025537_create_campaigns.sql index 0bffeb16..5545b636 100644 --- a/backend/migrations/20240406025537_create_campaigns.sql +++ b/backend/migrations/20240406025537_create_campaigns.sql @@ -31,3 +31,5 @@ CREATE TABLE campaign_roles ( ON DELETE CASCADE ON UPDATE CASCADE ) + +CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id); diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql index 2e2bb350..7cd38eb4 100644 --- a/backend/migrations/20240406031400_create_questions.sql +++ b/backend/migrations/20240406031400_create_questions.sql @@ -26,3 +26,5 @@ CREATE TABLE multi_option_question_options ( ON DELETE CASCADE ON UPDATE CASCADE ) + +CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id); diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index e39ede9f..ea30636c 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -36,6 +36,9 @@ CREATE TABLE application_roles ( ON UPDATE CASCADE ); +CREATE INDEX IDX_application_roles_applications on application_roles (application_id); +CREATE INDEX IDX_application_roles_campaign_roles on application_roles (campaign_role_id); + CREATE TABLE answers ( id SERIAL PRIMARY KEY, application_id BIGINT NOT NULL, @@ -52,6 +55,9 @@ CREATE TABLE answers ( ON UPDATE CASCADE ); +CREATE INDEX IDX_answers_applications on answers (application_id); +CREATE INDEX IDX_answers_questions on answers (question_id); + CREATE TABLE short_answer_answers ( id SERIAL PRIMARY KEY, text TEXT NOT NULL, @@ -63,6 +69,8 @@ CREATE TABLE short_answer_answers ( ON UPDATE CASCADE ); +CREATE INDEX IDX_multi_option_answer_options_answers on short_answer_answers (answer_id); + CREATE TABLE multi_option_answer_options ( id SERIAL PRIMARY KEY, option_id BIGINT NOT NULL, @@ -79,6 +87,9 @@ CREATE TABLE multi_option_answer_options ( ON UPDATE CASCADE ); +CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_answer_options (option_id); +CREATE INDEX IDX_multi_option_answer_options_answers on multi_option_answer_options (answer_id); + CREATE TABLE application_ratings ( id SERIAL PRIMARY KEY, application_id BIGINT NOT NULL, @@ -97,3 +108,6 @@ CREATE TABLE application_ratings ( ON DELETE CASCADE ON UPDATE CASCADE ); + +CREATE INDEX IDX_application_ratings_applications on application_ratings (application_id); +CREATE INDEX IDX_application_ratings_users on application_ratings (rater_id); From cadc6c6a0cfbf0fe2531ef7e8ad0e7fb3595373d Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 11:53:43 +1000 Subject: [PATCH 06/36] fix(db): index naming and missing semicolons --- backend/migrations/20240406024211_create_organisations.sql | 2 +- backend/migrations/20240406025537_create_campaigns.sql | 2 +- backend/migrations/20240406031400_create_questions.sql | 2 +- backend/migrations/20240406031915_create_applications.sql | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/migrations/20240406024211_create_organisations.sql b/backend/migrations/20240406024211_create_organisations.sql index 48f41aee..88dda366 100644 --- a/backend/migrations/20240406024211_create_organisations.sql +++ b/backend/migrations/20240406024211_create_organisations.sql @@ -15,6 +15,6 @@ CREATE TABLE organisation_admins ( REFERENCES organisations(id) ON DELETE CASCADE ON UPDATE CASCADE -) +); CREATE INDEX IDX_organisation_admins_organisation on organisation_admins (organisation_id); diff --git a/backend/migrations/20240406025537_create_campaigns.sql b/backend/migrations/20240406025537_create_campaigns.sql index 5545b636..46bca9df 100644 --- a/backend/migrations/20240406025537_create_campaigns.sql +++ b/backend/migrations/20240406025537_create_campaigns.sql @@ -30,6 +30,6 @@ CREATE TABLE campaign_roles ( REFERENCES campaigns(id) ON DELETE CASCADE ON UPDATE CASCADE -) +); CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id); diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql index 7cd38eb4..fa170813 100644 --- a/backend/migrations/20240406031400_create_questions.sql +++ b/backend/migrations/20240406031400_create_questions.sql @@ -25,6 +25,6 @@ CREATE TABLE multi_option_question_options ( REFERENCES questions(id) ON DELETE CASCADE ON UPDATE CASCADE -) +); CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id); diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index ea30636c..91d9edcf 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -62,14 +62,14 @@ CREATE TABLE short_answer_answers ( id SERIAL PRIMARY KEY, text TEXT NOT NULL, answer_id INTEGER NOT NULL, - CONSTRAINT FK_multi_option_answer_options_answers + CONSTRAINT FK_short_answer_answers_answers FOREIGN KEY(answer_id) REFERENCES answers(id) ON DELETE CASCADE ON UPDATE CASCADE ); -CREATE INDEX IDX_multi_option_answer_options_answers on short_answer_answers (answer_id); +CREATE INDEX IDX_short_answer_answers_answers on short_answer_answers (answer_id); CREATE TABLE multi_option_answer_options ( id SERIAL PRIMARY KEY, From 3c397bf0a1dbf297384e209c40aa8d8c5fa5b1f9 Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 13:30:03 +1000 Subject: [PATCH 07/36] dep(backend): update axum --- backend/server/Cargo.toml | 30 +++++++-------- backend/server/src/main.rs | 21 +++++++--- backend/server/src/models/auth.rs | 62 +++++++++++++++++------------- backend/server/src/service/auth.rs | 9 +++-- 4 files changed, 72 insertions(+), 50 deletions(-) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 85671315..fd764748 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -7,20 +7,20 @@ edition = "2021" [dependencies] # Primary crates -tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] } -axum = { version = "0.6.20", features = ["macros", "headers"] } -axum-extra = "0.8.0" -sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] } +tokio = { version = "1.34", features = ["macros", "rt-multi-thread"] } +axum = { version = "0.7", features = ["macros"] } +axum-extra = { version = "0.9", features = ["typed-header"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] } # Important secondary crates -anyhow = "1.0.75" -serde = { version = "1.0.188", features = ["derive"] } -reqwest = { version = "0.11.20", features = ["json"] } -serde_json = "1.0.105" -chrono = { version = "0.4.26", features = ["serde"] } -oauth2 = "4.4.1" -log = "0.4.20" -uuid = { version = "1.5.0", features = ["serde", "v4"] } -rs-snowflake = "0.6.0" -jsonwebtoken = "9.1.0" -dotenvy = "0.15.7" +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +reqwest = { version = "0.11", features = ["json"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +oauth2 = "4.4" +log = "0.4" +uuid = { version = "1.5", features = ["serde", "v4"] } +rs-snowflake = "0.6" +jsonwebtoken = "9.1" +dotenvy = "0.15" diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index e9d84481..9c807bf2 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,10 +1,15 @@ use anyhow::Result; use axum::{routing::get, Router}; -use jsonwebtoken::{DecodingKey, EncodingKey}; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use std::env; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use crate::handler::auth::{jwt_create, jwt_test}; +use crate::models::auth::AuthUser; + mod handler; mod models; mod service; @@ -30,6 +35,10 @@ async fn main() -> Result<()> { // let jwt_secret = "I want to cry"; let encoding_key = EncodingKey::from_secret(jwt_secret.as_bytes()); let decoding_key = DecodingKey::from_secret(jwt_secret.as_bytes()); + let mut jwt_header = Header::new(Algorithm::HS512); + let mut jwt_validator = Validation::new(Algorithm::HS512); + jwt_validator.set_issuer(&["Chaos"]); + jwt_validator.set_audience(&["chaos.devsoc.app"]); // Initialise reqwest client let ctx = reqwest::Client::new(); @@ -43,17 +52,19 @@ async fn main() -> Result<()> { ctx, encoding_key, decoding_key, + jwt_header, + jwt_validator, snowflake_generator, }; let app = Router::new() .route("/", get(|| async { "Hello, World!" })) + .route("/jwt", get(jwt_test)) + .route("/jwt_create", get(jwt_create)) .with_state(state); - axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); Ok(()) } diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 3fb447bb..8402bd85 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -1,14 +1,18 @@ use crate::models::app::AppState; use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; -use axum::extract::{FromRef, FromRequestParts, TypedHeader}; +use axum::extract::{FromRef, FromRequestParts}; use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; use axum::{ - async_trait, headers, + async_trait, http::{self, Request}, RequestPartsExt, }; +use axum_extra::{ + headers::Cookie, + TypedHeader, +}; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] @@ -47,18 +51,20 @@ 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 extracted_cookies = parts.extract::>().await; + let jwt_validator = &app_state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .await + .map_err(|_| AuthRedirect)?; - if let Ok(cookies) = extracted_cookies { - let token = cookies.get("auth_token").ok_or(AuthRedirect)?; - let claims = decode_auth_token(token.to_string(), decoding_key).ok_or(AuthRedirect)?; + let token = cookies.get("auth_token").unwrap(); - Ok(AuthUser { - user_id: claims.sub, - }) - } else { - Err(AuthRedirect) - } + let claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(AuthRedirect)?; + + Ok(AuthUser { + user_id: claims.sub, + }) } } @@ -78,21 +84,23 @@ 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 extracted_cookies = parts.extract::>().await; - - if let Ok(cookies) = extracted_cookies { - let token = cookies.get("auth_token").ok_or(AuthRedirect)?; - let claims = decode_auth_token(token.to_string(), decoding_key).ok_or(AuthRedirect)?; - - 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, - }); - } + let jwt_validator = &app_state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .await + .map_err(|_| AuthRedirect)?; + + let token = cookies.get("auth_token").unwrap(); + + let claims = decode_auth_token(token, decoding_key, jwt_validator).ok_or(AuthRedirect)?; + 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, + }); } } diff --git a/backend/server/src/service/auth.rs b/backend/server/src/service/auth.rs index 70b1bc0c..498159c4 100644 --- a/backend/server/src/service/auth.rs +++ b/backend/server/src/service/auth.rs @@ -15,9 +15,12 @@ pub async fn create_or_get_user_id( pool: Pool, mut snowflake_generator: SnowflakeIdGenerator, ) -> Result { - let possible_user_id = sqlx::query!("SELECT id FROM users WHERE lower(email) = $1", email.to_lowercase()) - .fetch_optional(&pool) - .await?; + let possible_user_id = sqlx::query!( + "SELECT id FROM users WHERE lower(email) = $1", + email.to_lowercase() + ) + .fetch_optional(&pool) + .await?; if let Some(result) = possible_user_id { return Ok(result.id); From 120f5e1d2531326a48bc89c85c838f6a4c480455 Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 13:51:36 +1000 Subject: [PATCH 08/36] fix(backend): remove testing jwt handlers --- backend/server/src/handler/auth.rs | 34 ++++++++++++------------------ backend/server/src/main.rs | 5 ----- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/backend/server/src/handler/auth.rs b/backend/server/src/handler/auth.rs index fe6effee..126c91da 100644 --- a/backend/server/src/handler/auth.rs +++ b/backend/server/src/handler/auth.rs @@ -1,14 +1,16 @@ use crate::models::app::AppState; -use crate::models::auth::{AuthRequest, GoogleUserProfile}; +use crate::models::auth::{AuthRequest, AuthUser, GoogleUserProfile}; use crate::service::auth::create_or_get_user_id; use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::response::IntoResponse; -use axum::Extension; +use axum::{Extension, Json}; use log::error; use oauth2::basic::BasicClient; use oauth2::reqwest::async_http_client; use oauth2::{AuthorizationCode, TokenResponse}; +use crate::models::error::ChaosError; +use crate::service::jwt::encode_auth_token; /// This function handles the passing in of the Google OAuth code. After allowing our app the /// requested permissions, the user is redirected to this url on our server, where we use the @@ -17,34 +19,23 @@ pub async fn google_callback( State(state): State, Query(query): Query, Extension(oauth_client): Extension, -) -> Result { - let token = match oauth_client +) -> Result { + let token = oauth_client .exchange_code(AuthorizationCode::new(query.code)) .request_async(async_http_client) - .await - { - Ok(res) => res, - Err(e) => { - error!("An error occured while exchanging Google OAuth code"); - return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())); - } - }; + .await?; - let profile = match state + let profile = state .ctx .get("https://openidconnect.googleapis.com/v1/userinfo") .bearer_auth(token.access_token().secret().to_owned()) .send() - .await - { - Ok(res) => res, - Err(e) => return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), - }; + .await?; let profile = profile.json::().await.unwrap(); let user_id = create_or_get_user_id( - profile.email, + profile.email.clone(), profile.name, state.db, state.snowflake_generator, @@ -52,6 +43,7 @@ pub async fn google_callback( .await .unwrap(); - // TODO: Create a JWT from this user_id and return to the user. - Ok("woohoo") + // TODO: Return JWT as set-cookie header. + let token = encode_auth_token(profile.email, user_id, &state.encoding_key, &state.jwt_header); + Ok(token) } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 9c807bf2..4f185702 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -5,10 +5,7 @@ use models::app::AppState; use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use std::env; -use axum::http::StatusCode; use axum::response::IntoResponse; -use crate::handler::auth::{jwt_create, jwt_test}; -use crate::models::auth::AuthUser; mod handler; mod models; @@ -59,8 +56,6 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) - .route("/jwt", get(jwt_test)) - .route("/jwt_create", get(jwt_create)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); From 5c1c462deaa22ed0fa3c375c71d7875a832ece68 Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 13:51:58 +1000 Subject: [PATCH 09/36] feat(backend): Add custom jwt validator and header --- backend/server/src/models/app.rs | 4 +++- backend/server/src/service/jwt.rs | 32 +++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index 4802de13..9fbd2a8e 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -1,4 +1,4 @@ -use jsonwebtoken::{DecodingKey, EncodingKey}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use reqwest::Client as ReqwestClient; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; @@ -9,5 +9,7 @@ pub struct AppState { pub ctx: ReqwestClient, pub decoding_key: DecodingKey, pub encoding_key: EncodingKey, + pub jwt_header: Header, + pub jwt_validator: Validation, pub snowflake_generator: SnowflakeIdGenerator, } diff --git a/backend/server/src/service/jwt.rs b/backend/server/src/service/jwt.rs index 272e6aed..8c5d06bc 100644 --- a/backend/server/src/service/jwt.rs +++ b/backend/server/src/service/jwt.rs @@ -1,5 +1,6 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use axum::extract::State; -use jsonwebtoken::{decode, Validation}; +use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation}; use jsonwebtoken::{Algorithm, DecodingKey}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -21,14 +22,37 @@ pub struct AuthorizationJwtPayload { pub username: String, // username } +pub fn encode_auth_token( + username: String, + user_id: i64, + encoding_key: &EncodingKey, + jwt_header: &Header, +) -> String { + let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let expiry = i64::try_from((current_time + Duration::from_secs(604800)).as_secs()).unwrap(); + let claims = AuthorizationJwtPayload { + iss: "Chaos".to_string(), + sub: user_id, + jti: Uuid::new_v4(), + aud: vec!["chaos.devsoc.app".to_string()], + exp: expiry, + nbf: i64::try_from(current_time.as_secs()).unwrap(), + iat: i64::try_from(current_time.as_secs()).unwrap(), + username + }; + + encode(jwt_header, &claims, encoding_key).unwrap() +} + pub fn decode_auth_token( - token: String, + token: &str, decoding_key: &DecodingKey, + jwt_validator: &Validation, ) -> Option { let decode_token = decode::( - token.as_str(), + token, decoding_key, - &Validation::new(Algorithm::HS256), + jwt_validator, ); match decode_token { From 156f67c37874433afd7e05f9b6e981fe3e92738f Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 13:52:15 +1000 Subject: [PATCH 10/36] feat(backend): basic error handling enum --- backend/server/src/models/auth.rs | 19 +++++++++-------- backend/server/src/models/error.rs | 33 ++++++++++++++++++++++++++++++ backend/server/src/models/mod.rs | 1 + 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 backend/server/src/models/error.rs diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 8402bd85..92cae8f5 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -14,6 +14,7 @@ use axum_extra::{ TypedHeader, }; use serde::{Deserialize, Serialize}; +use crate::models::error::ChaosError; #[derive(Deserialize, Serialize)] pub struct AuthRequest { @@ -46,7 +47,7 @@ where AppState: FromRef, S: Send + Sync, { - type Rejection = AuthRedirect; + type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); @@ -55,12 +56,12 @@ where let TypedHeader(cookies) = parts .extract::>() .await - .map_err(|_| AuthRedirect)?; + .map_err(|_| ChaosError::NotLoggedIn)?; - let token = cookies.get("auth_token").unwrap(); + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(AuthRedirect)?; + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; Ok(AuthUser { user_id: claims.sub, @@ -79,7 +80,7 @@ where AppState: FromRef, S: Send + Sync, { - type Rejection = AuthRedirect; + type Rejection = ChaosError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); @@ -88,11 +89,11 @@ where let TypedHeader(cookies) = parts .extract::>() .await - .map_err(|_| AuthRedirect)?; + .map_err(|_| ChaosError::NotLoggedIn)?; - let token = cookies.get("auth_token").unwrap(); + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; - let claims = decode_auth_token(token, decoding_key, jwt_validator).ok_or(AuthRedirect)?; + 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; @@ -104,6 +105,6 @@ where } } - Err(AuthRedirect) + Err(ChaosError::NotLoggedIn) } } diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs new file mode 100644 index 00000000..956fdede --- /dev/null +++ b/backend/server/src/models/error.rs @@ -0,0 +1,33 @@ +use anyhow::Error; +use axum::extract::rejection::JsonRejection; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Redirect, Response}; + +pub enum ChaosError { + NotLoggedIn, + Unauthorized, + ForbiddenOperation, + ServerError(anyhow::Error) +} + +impl IntoResponse for ChaosError { + fn into_response(self) -> Response { + match self { + ChaosError::NotLoggedIn => { + Redirect::temporary("/auth/google").into_response() + } + ChaosError::Unauthorized => {(StatusCode::UNAUTHORIZED, "Unauthorized".to_string()).into_response()}, + ChaosError::ForbiddenOperation => {(StatusCode::FORBIDDEN, "Forbidden operation".to_string()).into_response()}, + ChaosError::ServerError(e) => {(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()} + } + } +} + +impl From for ChaosError +where + E: Into, +{ + fn from(err: E) -> Self { + ChaosError::ServerError(err.into()) + } +} diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index 2164071c..b992e0f9 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -1,3 +1,4 @@ pub mod app; pub mod auth; +pub mod error; pub mod user; From a7483bc95ba418af301e7a3408ca04c41b31ac24 Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 13:56:07 +1000 Subject: [PATCH 11/36] fix(backend): ran cargo fmt --- backend/server/src/handler/auth.rs | 11 ++++++++--- backend/server/src/main.rs | 2 +- backend/server/src/models/auth.rs | 10 ++++------ backend/server/src/models/error.rs | 12 +++++------- backend/server/src/service/jwt.rs | 10 +++------- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/backend/server/src/handler/auth.rs b/backend/server/src/handler/auth.rs index 126c91da..ed5a94f0 100644 --- a/backend/server/src/handler/auth.rs +++ b/backend/server/src/handler/auth.rs @@ -1,6 +1,8 @@ use crate::models::app::AppState; use crate::models::auth::{AuthRequest, AuthUser, GoogleUserProfile}; +use crate::models::error::ChaosError; use crate::service::auth::create_or_get_user_id; +use crate::service::jwt::encode_auth_token; use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::response::IntoResponse; @@ -9,8 +11,6 @@ use log::error; use oauth2::basic::BasicClient; use oauth2::reqwest::async_http_client; use oauth2::{AuthorizationCode, TokenResponse}; -use crate::models::error::ChaosError; -use crate::service::jwt::encode_auth_token; /// This function handles the passing in of the Google OAuth code. After allowing our app the /// requested permissions, the user is redirected to this url on our server, where we use the @@ -44,6 +44,11 @@ pub async fn google_callback( .unwrap(); // TODO: Return JWT as set-cookie header. - let token = encode_auth_token(profile.email, user_id, &state.encoding_key, &state.jwt_header); + let token = encode_auth_token( + profile.email, + user_id, + &state.encoding_key, + &state.jwt_header, + ); Ok(token) } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 4f185702..4a7937a5 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,11 +1,11 @@ use anyhow::Result; +use axum::response::IntoResponse; use axum::{routing::get, Router}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use std::env; -use axum::response::IntoResponse; mod handler; mod models; diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 92cae8f5..e3154fc7 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -1,4 +1,5 @@ use crate::models::app::AppState; +use crate::models::error::ChaosError; use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; use axum::extract::{FromRef, FromRequestParts}; @@ -9,12 +10,8 @@ use axum::{ http::{self, Request}, RequestPartsExt, }; -use axum_extra::{ - headers::Cookie, - TypedHeader, -}; +use axum_extra::{headers::Cookie, TypedHeader}; use serde::{Deserialize, Serialize}; -use crate::models::error::ChaosError; #[derive(Deserialize, Serialize)] pub struct AuthRequest { @@ -93,7 +90,8 @@ where 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 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; diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 956fdede..f5b591b9 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -7,18 +7,16 @@ pub enum ChaosError { NotLoggedIn, Unauthorized, ForbiddenOperation, - ServerError(anyhow::Error) + ServerError(anyhow::Error), } impl IntoResponse for ChaosError { fn into_response(self) -> Response { match self { - ChaosError::NotLoggedIn => { - Redirect::temporary("/auth/google").into_response() - } - ChaosError::Unauthorized => {(StatusCode::UNAUTHORIZED, "Unauthorized".to_string()).into_response()}, - ChaosError::ForbiddenOperation => {(StatusCode::FORBIDDEN, "Forbidden operation".to_string()).into_response()}, - ChaosError::ServerError(e) => {(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()} + ChaosError::NotLoggedIn => Redirect::temporary("/auth/google").into_response(), + ChaosError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), + ChaosError::ForbiddenOperation => (StatusCode::FORBIDDEN, "Forbidden operation").into_response(), + ChaosError::ServerError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() } } } diff --git a/backend/server/src/service/jwt.rs b/backend/server/src/service/jwt.rs index 8c5d06bc..29fcf1fa 100644 --- a/backend/server/src/service/jwt.rs +++ b/backend/server/src/service/jwt.rs @@ -1,8 +1,8 @@ -use std::time::{Duration, SystemTime, UNIX_EPOCH}; use axum::extract::State; use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation}; use jsonwebtoken::{Algorithm, DecodingKey}; use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use uuid::Uuid; use crate::AppState; @@ -38,7 +38,7 @@ pub fn encode_auth_token( exp: expiry, nbf: i64::try_from(current_time.as_secs()).unwrap(), iat: i64::try_from(current_time.as_secs()).unwrap(), - username + username, }; encode(jwt_header, &claims, encoding_key).unwrap() @@ -49,11 +49,7 @@ pub fn decode_auth_token( decoding_key: &DecodingKey, jwt_validator: &Validation, ) -> Option { - let decode_token = decode::( - token, - decoding_key, - jwt_validator, - ); + let decode_token = decode::(token, decoding_key, jwt_validator); match decode_token { Ok(token) => Option::from(token.claims), From 25f43ea00779e50bb8eb20e157470f5dba414817 Mon Sep 17 00:00:00 2001 From: Kavika Date: Sun, 7 Apr 2024 14:06:24 +1000 Subject: [PATCH 12/36] fix(backend): remove unused imports --- backend/server/src/handler/auth.rs | 6 ++---- backend/server/src/main.rs | 5 +++-- backend/server/src/models/auth.rs | 1 - backend/server/src/models/error.rs | 6 ++++-- backend/server/src/service/auth.rs | 5 ++--- backend/server/src/service/jwt.rs | 5 +---- backend/server/src/service/oauth2.rs | 3 ++- 7 files changed, 14 insertions(+), 17 deletions(-) diff --git a/backend/server/src/handler/auth.rs b/backend/server/src/handler/auth.rs index ed5a94f0..12fcab02 100644 --- a/backend/server/src/handler/auth.rs +++ b/backend/server/src/handler/auth.rs @@ -1,13 +1,11 @@ use crate::models::app::AppState; -use crate::models::auth::{AuthRequest, AuthUser, GoogleUserProfile}; +use crate::models::auth::{AuthRequest, GoogleUserProfile}; use crate::models::error::ChaosError; use crate::service::auth::create_or_get_user_id; use crate::service::jwt::encode_auth_token; use axum::extract::{Query, State}; -use axum::http::StatusCode; use axum::response::IntoResponse; -use axum::{Extension, Json}; -use log::error; +use axum::Extension; use oauth2::basic::BasicClient; use oauth2::reqwest::async_http_client; use oauth2::{AuthorizationCode, TokenResponse}; diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 4a7937a5..8d91e4b0 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,11 +1,11 @@ use anyhow::Result; -use axum::response::IntoResponse; use axum::{routing::get, Router}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use std::env; +use crate::handler::auth::google_callback; mod handler; mod models; @@ -32,7 +32,7 @@ async fn main() -> Result<()> { // let jwt_secret = "I want to cry"; let encoding_key = EncodingKey::from_secret(jwt_secret.as_bytes()); let decoding_key = DecodingKey::from_secret(jwt_secret.as_bytes()); - let mut jwt_header = Header::new(Algorithm::HS512); + let jwt_header = Header::new(Algorithm::HS512); let mut jwt_validator = Validation::new(Algorithm::HS512); jwt_validator.set_issuer(&["Chaos"]); jwt_validator.set_audience(&["chaos.devsoc.app"]); @@ -56,6 +56,7 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) + .route("/api/auth/callback/google", get(google_callback)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index e3154fc7..149c3a25 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -7,7 +7,6 @@ use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; use axum::{ async_trait, - http::{self, Request}, RequestPartsExt, }; use axum_extra::{headers::Cookie, TypedHeader}; diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index f5b591b9..ca160d97 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -1,8 +1,10 @@ -use anyhow::Error; -use axum::extract::rejection::JsonRejection; use axum::http::StatusCode; use axum::response::{IntoResponse, Redirect, Response}; +/// Custom error enum for Chaos. +/// +/// Handles all anyhow errors (when `?` is used) alongside +/// specific errors for business logic. pub enum ChaosError { NotLoggedIn, Unauthorized, diff --git a/backend/server/src/service/auth.rs b/backend/server/src/service/auth.rs index 498159c4..7da5a35b 100644 --- a/backend/server/src/service/auth.rs +++ b/backend/server/src/service/auth.rs @@ -1,8 +1,7 @@ -use crate::models::user::UserRole; use anyhow::Result; -use jsonwebtoken::{DecodingKey, EncodingKey}; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; +use crate::models::user::UserRole; /// 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. @@ -28,7 +27,7 @@ pub async fn create_or_get_user_id( let user_id = snowflake_generator.real_time_generate(); - let response = sqlx::query!( + sqlx::query!( "INSERT INTO users (id, email, name) VALUES ($1, $2, $3)", user_id, email.to_lowercase(), diff --git a/backend/server/src/service/jwt.rs b/backend/server/src/service/jwt.rs index 29fcf1fa..c7db35cb 100644 --- a/backend/server/src/service/jwt.rs +++ b/backend/server/src/service/jwt.rs @@ -1,12 +1,9 @@ -use axum::extract::State; use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation}; -use jsonwebtoken::{Algorithm, DecodingKey}; +use jsonwebtoken::DecodingKey; use serde::{Deserialize, Serialize}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use uuid::Uuid; -use crate::AppState; - #[derive(Debug, Deserialize, Serialize)] pub struct AuthorizationJwtPayload { pub iss: String, // issuer diff --git a/backend/server/src/service/oauth2.rs b/backend/server/src/service/oauth2.rs index 673379fc..43c1104c 100644 --- a/backend/server/src/service/oauth2.rs +++ b/backend/server/src/service/oauth2.rs @@ -3,12 +3,13 @@ use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; use std::env; /// Returns a oauth2::BasicClient, setup with settings for CHAOS Google OAuth. +/// /// Client follows OAuth2 Standard (https://oauth.net/2/) to get user's email /// using OpenID Connect (https://openid.net/developers/how-connect-works/). pub fn build_oauth_client(client_id: String, client_secret: String) -> BasicClient { let hostname = env::var("CHAOS_HOSTNAME").expect("Could not read CHAOS hostname"); - let redirect_url = format!("{}/api/auth/google_callback", hostname); + let redirect_url = format!("{}/api/auth/callback/google", hostname); let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()) .expect("Invalid authorization endpoint URL"); From 2f26dbf41966792edfe207fbccd9f44ee4cf474b Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 11 Apr 2024 22:07:15 +1000 Subject: [PATCH 13/36] CRUD operations - awaiting Campaign - haven't enforced db safety --- backend/server/Cargo.toml | 2 +- backend/server/src/handler/organisation.rs | 71 +++++-- backend/server/src/main.rs | 20 +- backend/server/src/models/organisation.rs | 2 +- backend/server/src/service/organisation.rs | 229 +++++++++++++++------ 5 files changed, 232 insertions(+), 92 deletions(-) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 85671315..86f5ccde 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] } axum = { version = "0.6.20", features = ["macros", "headers"] } axum-extra = "0.8.0" -sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] } +sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "postgres", "time", "uuid", "chrono"] } # Important secondary crates anyhow = "1.0.75" diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 43ba1e4a..44a998c8 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,5 +1,6 @@ use crate::models::app::AppState; use crate::models::auth::AuthUser; +use crate::models::auth::SuperUser; use axum::http::StatusCode; use crate::models; use crate::service; @@ -18,48 +19,56 @@ pub async fn get_organisations( pub async fn get_organisation( State(state): State, - Path(organisation_id): Path, - _user: AuthUser + Path(organisation_id): Path ) -> Result { match service::organisation::get_organisation(organisation_id, state.db).await { - Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), + Ok(organisation_details) => Ok((StatusCode::OK, Json(organisation_details))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } } -pub async fn update_organisation( +pub async fn update_organisation_logo( State(state): State, Path(organisation_id): Path, - _user: AuthUser, + _user: SuperUser, Json(request_body): Json, ) -> Result { - match service::organisation::update_organisation(organisation_id, Some(request_body.name), request_body.logo, state.db).await { - Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + match request_body.logo { + Some(logo) => { + // Logo exists, proceed with the update + match service::organisation::update_organisation_logo(organisation_id, logo, state.db).await { + Ok(logo_url) => Ok((StatusCode::OK, Json(logo_url))), + Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), + } + } + None => { + // Logo is missing, return an error + Err((StatusCode::BAD_REQUEST, "Logo is missing".to_string())) + } } } pub async fn delete_organisation( State(state): State, Path(organisation_id): Path, - _user: AuthUser + _user: SuperUser ) -> Result { match service::organisation::delete_organisation(organisation_id, _user.user_id, state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + Err(e) => return Err((StatusCode::UNAUTHORIZED, e.to_string())), } } pub async fn create_organisation( State(state): State, - _user: AuthUser, + _user: SuperUser, Json(request_body): Json ) -> Result { let mut snowflake_generator = state.snowflake_generator; let new_organisation_id = snowflake_generator.real_time_generate(); - match service::organisation::create_organisation(new_organisation_id, request_body.name, request_body.logo, state.db).await { - Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), + match service::organisation::create_organisation(new_organisation_id, request_body.name, state.db).await { + Ok(message) => Ok((StatusCode::OK, Json(message))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } } @@ -67,7 +76,7 @@ pub async fn create_organisation( pub async fn get_organisation_admins( State(state): State, Path(organisation_id): Path, - _user: AuthUser + _user: SuperUser ) -> Result { match service::organisation::get_organisation_admins(organisation_id, state.db).await { Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), @@ -75,12 +84,16 @@ pub async fn get_organisation_admins( } } -pub async fn add_admin_to_organisation( +pub struct IdList { + members: Vec +} +pub async fn update_organisation_admins( State(state): State, Path(organisation_id): Path, - _user: AuthUser + Json(request_body): Json, + _user: SuperUser ) -> Result { - match service::organisation::add_admin_to_organisation(organisation_id, _user.user_id, state.db).await { + match service::organisation::update_organisation_admins(organisation_id, request_body.members, state.db).await { Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -95,4 +108,28 @@ pub async fn remove_admin_from_organisation( Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } +} + +pub async fn get_organisation_campaigns( + State(state): State, + Path(organisation_id): Path, +) -> Result { + match service::organisation::get_organisation_campaigns(organisation_id, state.db).await { + Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } +} + +pub async fn create_campaign_for_organisation( + State(state): State, + _user: SuperUser, + Json(request_body): Json +) -> Result { + let mut snowflake_generator = state.snowflake_generator; + let new_campaign_id = snowflake_generator.real_time_generate(); + + match service::organisation::create_campaign_for_organisation(new_campaign_id, request_body.name, request_body.description, request_body.starts_at, request_body.ends_at, state.db).await { + Ok(message) => Ok((StatusCode::OK, Json(message))), + Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + } } \ No newline at end of file diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index ff81fcc1..f2ecbce8 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use axum::{routing::get, Router}; +use axum::{routing::get, Router, routing::post}; use jsonwebtoken::{DecodingKey, EncodingKey}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; @@ -49,18 +49,24 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) .route("/api/v1/organisations", - get(handler::organisation::get_organisations) - .post(handler::organisation::create_organisation) + post(handler::organisation::create_organisation) ) .route("/api/v1/organisations/:organisation_id", get(handler::organisation::get_organisation) - .patch(handler::organisation::update_organisation) .delete(handler::organisation::delete_organisation) ) - .route("/api/v1/organisations/admin/:organisation_id", + .route("/api/v1/organisations/:organisation_id/campaigns", + get(handler::organisation::get_organisation_campaigns) + ) + .route("/api/v1/organisations/:organisation_id/logo", + post(handler::organisation::update_organisation_logo) + ) + .route("/api/v1/organisations/:organisation_id/members", get(handler::organisation::get_organisation_admins) - .post(handler::organisation::add_admin_to_organisation) - .delete(handler::organisation::remove_admin_from_organisation) + .put(handler::organisation::update_organisation_admins) + ) + .route("/api/v1/organisations/:organisation_id/campaign", + post(handler::organisation::create_campaign_for_organisation) ) .with_state(state); diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index efa4a8b6..37798da0 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -9,7 +9,7 @@ pub struct Organisation { pub logo: Option, pub created_at: DateTime, pub updated_at: DateTime, - // pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done + pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done pub organisation_admins: Vec } diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index f5698dbc..421c321e 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,23 +1,18 @@ use anyhow::{bail, Result}; -use sqlx::{types::time::{OffsetDateTime, PrimitiveDateTime}, Pool, Postgres}; +use sqlx::{Pool, Postgres}; +use chrono::{DateTime, Local, NaiveDateTime, Utc}; +use serde::{Deserialize, Serialize}; -pub async fn get_organisations(pool: Pool) -> Result> { - let organisations = sqlx::query!("SELECT * FROM organisations") - .fetch_all(&pool) - .await?; - - let mut name_list: Vec = Vec::new(); - - for organisation in organisations { - name_list.push(organisation.name); - } - - Ok(name_list) +#[derive(Deserialize, Serialize)] +pub struct OrganisationDetails { + pub id: i64, + pub name: String, + pub logo: Option, + pub created_at: DateTime } - -pub async fn get_organisation(id: i64, pool: Pool) -> Result { - let org_name = sqlx::query! +pub async fn get_organisation(id: i64, pool: Pool) -> Result { + let organisation_details = sqlx::query! ( " SELECT * @@ -27,41 +22,44 @@ pub async fn get_organisation(id: i64, pool: Pool) -> Result { id).fetch_optional(&pool) .await?; - if let Some(result) = org_name { - return Ok(result.name); + if let Some(result) = organisation_details { + let details = OrganisationDetails { + id: result.id, + name: result.name, + logo: result.logo, + created_at: result.created_at.and_utc() + }; + return Ok(details); } bail!("error: failed to get organisation"); } -pub async fn update_organisation(id: i64, name: Option, logo: Option, pool: Pool) -> Result { - let current_time = OffsetDateTime::now_utc(); - let primitive_time = PrimitiveDateTime::new(current_time.date(), current_time.time()); + +pub async fn update_organisation_logo(id: i64, logo: String, pool: Pool) -> Result { + let dt = Local::now(); + let current_time = dt.naive_utc(); let updated_organisation = sqlx::query! ( " UPDATE organisations - SET name = $2, logo = $3, updated_at = $4 + SET logo = $2, updated_at = $3 WHERE id = $1 RETURNING * ", id, - name, logo, - primitive_time + current_time ).fetch_one(&pool) .await; match updated_organisation { - Ok(_) => { - println!("{:?}", updated_organisation); - return Ok("done".to_string()) - }, - Err(_) => bail!("error: Failed to update organisation") + Ok(_) => Ok(logo), + Err(_) => bail!("error: Failed to update organisation logo") } } -pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { +pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { // Delete all organisation_admins with the corresponding organisation_id let deleted_users = sqlx::query! ( @@ -87,10 +85,8 @@ pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool .await; match deleted_users { - Ok(_) => { - println!("{:?}", deleted_users); - }, - Err(_) => bail!("error: user is not an admin of the organisation") + Ok(_) => {}, + Err(_) => return Err("Unauthorized".to_string()) } @@ -107,37 +103,39 @@ pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool match deleted_organisation { Ok(_) => { - println!("{:?}", deleted_organisation); - Ok("Delete organisation and all the admins from database".to_string()) + Ok("Successfully deleted organisation.".to_string()) }, - Err(_) => bail!("error: user is not an admin of the organisation") + Err(_) => Err("Unauthorized".to_string()) } } -pub async fn create_organisation(id: i64, name: String, logo: Option, pool: Pool) -> Result { +pub async fn create_organisation(id: i64, name: String, pool: Pool) -> Result { let new_organisation = sqlx::query! ( " - INSERT INTO organisations (id, name, logo) - VALUES ($1, $2, $3) + INSERT INTO organisations (id, name) + VALUES ($1, $2) RETURNING * ", id, - name, - logo + name ).fetch_one(&pool).await; match new_organisation { Ok(_) => { - println!("{:?}", new_organisation); - Ok("Created new organisation in database".to_string()) + Ok("Successfully created organisation.".to_string()) }, Err(_) => bail!("error: failed to create organisation") } } -pub async fn get_organisation_admins(organisation_id: i64, pool: Pool) -> Result> { +#[derive(Deserialize, Serialize)] +pub struct Member { + pub id: i64, + pub name: String +} +pub async fn get_organisation_admins(organisation_id: i64, pool: Pool) -> Result> { let admin_list = sqlx::query! ( " @@ -148,22 +146,68 @@ pub async fn get_organisation_admins(organisation_id: i64, pool: Pool) , organisation_id ).fetch_all(&pool).await?; - let mut admin_id_list: Vec = Vec::new(); + let admin_id_list: Vec = admin_list.into_iter().map(|row| row.user_id).collect(); - for admin in admin_list { - admin_id_list.push(admin.user_id); + if admin_id_list.is_empty() { + return Ok(Vec::new()); } - Ok(admin_id_list) + let mut users = Vec::new(); + + for id in admin_id_list { + let user_object = sqlx::query!( + " + SELECT id, name + FROM users + WHERE id = $1 + " + , id + + ) + .fetch_one(&pool) + .await?; + users.push( + Member { + id: user_object.id, + name: user_object.name + } + ) + } + + Ok(users) } -pub async fn add_admin_to_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { +pub async fn update_organisation_admins(organisation_id: i64, admin_id_list: Vec, pool: Pool) -> Result { + sqlx::query!( + "DELETE FROM organisation_admins WHERE organisation_id = $1", + organisation_id + ) + .execute(&pool) + .await?; + + for admin_id in admin_id_list { + sqlx::query!( + " + INSERT INTO organisation_admins (organisation_id, user_id) + VALUES ($1, $2) + ", + organisation_id, + admin_id + ) + .execute(&pool) + .await?; + } + + Ok("Successfully updated members.".to_string()) +} + +pub async fn remove_admin_from_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { let new_organisation_admin = sqlx::query! ( " - INSERT INTO organisation_admins - (organisation_id, user_id) - VALUES ($1, $2) + DELETE FROM organisation_admins + WHERE organisation_id = $1 + AND user_id = $2 RETURNING * ", organisation_id, @@ -173,30 +217,83 @@ pub async fn add_admin_to_organisation(organisation_id: i64, admin_id: i64, pool match new_organisation_admin { Ok(_) => { println!("{:?}", new_organisation_admin); - Ok("Created new organisation admin in database".to_string()) + Ok("Deleted organisation admin in database".to_string()) }, - Err(_) => bail!("error: failed to add admin to organisation") + Err(_) => bail!("error: failed to remove admin from organisation") } } -pub async fn remove_admin_from_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { - let new_organisation_admin = sqlx::query! + +#[derive(Deserialize, Serialize)] +pub struct Campaign { + // Define your struct fields based on the Campaign model + // For example: + pub id: i64, + pub name: String, + pub cover_image: Option, + pub description: String, + pub starts_at: DateTime, + pub ends_at: DateTime, +} +pub async fn get_organisation_campaigns(id: i64, pool: Pool) -> Result> { + let campaigns = sqlx::query! ( " - DELETE FROM organisation_admins + SELECT * + FROM campaigns WHERE organisation_id = $1 - AND user_id = $2 + ", + id + ).fetch_all(&pool).await; + + let mut result: Vec = Vec::new(); + match campaigns { + Ok(campaign_list) => { + for campaign in campaign_list { + let details = Campaign { + id: campaign.id, + name: campaign.name, + cover_image: campaign.cover_image, + description: campaign.description, + starts_at: campaign.starts_at.and_utc(), + ends_at: campaign.ends_at.and_utc() + }; + result.push(details); + } + }, + Err(_) => bail!("Error getting organisation campaign") + } + Ok(result) +} + +pub async fn create_campaign_for_organisation(id: i64, name: String, description: String, starts_at: NaiveDateTime, ends_at: NaiveDateTime, pool: Pool) -> Result { + let new_campaign = sqlx::query! + ( + " + INSERT INTO campaigns (id, name, description, starts_at, ends_at) + VALUES ($1, $2, $3, $4, $5) RETURNING * ", - organisation_id, - admin_id + id, + name, + description, + starts_at, + ends_at ).fetch_one(&pool).await; - match new_organisation_admin { - Ok(_) => { - println!("{:?}", new_organisation_admin); - Ok("Deleted organisation admin in database".to_string()) + match new_campaign { + Ok(res) => { + Ok( + Campaign { + id: res.id, + name: res.name, + cover_image: None, + description: res.description, + starts_at: res.starts_at.and_utc(), + ends_at: res.ends_at.and_utc() + } + ) }, - Err(_) => bail!("error: failed to remove admin from organisation") + Err(_) => bail!("error: failed to create campaign") } } \ No newline at end of file From f61e1df62151bfbbe6ecf1f8a0a21123491d4048 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 5 Jun 2024 21:00:26 +1000 Subject: [PATCH 14/36] implement feedback --- backend/server/src/models/organisation.rs | 12 +- backend/server/src/service/organisation.rs | 253 +++++++-------------- 2 files changed, 88 insertions(+), 177 deletions(-) diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 37798da0..ee8206f5 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -10,11 +10,13 @@ pub struct Organisation { pub created_at: DateTime, pub updated_at: DateTime, pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done - pub organisation_admins: Vec + pub organisation_admins: Vec } -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct OrganisationAdmins { - pub organisation_id: i64, // References id in Organisation table - pub user_id: i64 // References id in User table +#[derive(Deserialize, Serialize)] +pub struct OrganisationDetails { + pub id: i64, + pub name: String, + pub logo: Option, + pub created_at: DateTime } diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index 421c321e..dc682369 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,15 +1,9 @@ -use anyhow::{bail, Result}; +use anyhow::{bail, Error, Result}; use sqlx::{Pool, Postgres}; use chrono::{DateTime, Local, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize)] -pub struct OrganisationDetails { - pub id: i64, - pub name: String, - pub logo: Option, - pub created_at: DateTime -} +use crate::models::organisation::OrganisationDetails; pub async fn get_organisation(id: i64, pool: Pool) -> Result { let organisation_details = sqlx::query! @@ -36,112 +30,63 @@ pub async fn get_organisation(id: i64, pool: Pool) -> Result) -> Result { +pub async fn update_organisation_logo(id: i64, logo: String, pool: Pool) -> Result<()> { let dt = Local::now(); let current_time = dt.naive_utc(); - let updated_organisation = sqlx::query! + sqlx::query! ( " UPDATE organisations SET logo = $2, updated_at = $3 WHERE id = $1 - RETURNING * ", id, logo, current_time ).fetch_one(&pool) - .await; + .await?; - match updated_organisation { - Ok(_) => Ok(logo), - Err(_) => bail!("error: Failed to update organisation logo") - } + Ok(()) } -pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { - // Delete all organisation_admins with the corresponding organisation_id - let deleted_users = sqlx::query! - ( - " - WITH user_exists AS ( - SELECT 1 AS exists_flag - FROM organisation_admins - WHERE organisation_id = $1 - AND user_id = $2 - LIMIT 1 - ) - DELETE FROM organisation_admins - WHERE organisation_id = $1 - AND EXISTS ( - SELECT 1 - FROM user_exists - ) - RETURNING * - ", - organisation_id, - admin_id - ).fetch_one(&pool) - .await; - - match deleted_users { - Ok(_) => {}, - Err(_) => return Err("Unauthorized".to_string()) - } - - +pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result<()> { // Delete organisation - let deleted_organisation = sqlx::query! + sqlx::query! ( " - DELETE FROM organisations WHERE id = $1 + CASCADE DELETE FROM organisations WHERE id = $1 RETURNING * ", organisation_id ).fetch_one(&pool) - .await; - - match deleted_organisation { - Ok(_) => { - Ok("Successfully deleted organisation.".to_string()) - }, - Err(_) => Err("Unauthorized".to_string()) -} + .await?; + Ok(()) } -pub async fn create_organisation(id: i64, name: String, pool: Pool) -> Result { - let new_organisation = sqlx::query! +pub async fn create_organisation(id: i64, name: String, pool: Pool) -> Result<(), Error> { + sqlx::query! ( " INSERT INTO organisations (id, name) VALUES ($1, $2) - RETURNING * ", id, name - ).fetch_one(&pool).await; - - match new_organisation { - Ok(_) => { - Ok("Successfully created organisation.".to_string()) - }, - Err(_) => bail!("error: failed to create organisation") - } + ).fetch_one(&pool) + .await?; + + Ok(()) } -#[derive(Deserialize, Serialize)] -pub struct Member { - pub id: i64, - pub name: String -} +// Below returns subset of user - not completed yet - remove comment when done pub async fn get_organisation_admins(organisation_id: i64, pool: Pool) -> Result> { let admin_list = sqlx::query! ( " - SELECT user_id - FROM organisation_admins - WHERE organisation_id = $1 + SELECT organisation_admins + FROM organisations + WHERE id = $1 " , organisation_id ).fetch_all(&pool).await?; @@ -155,75 +100,57 @@ pub async fn get_organisation_admins(organisation_id: i64, pool: Pool) let mut users = Vec::new(); for id in admin_id_list { - let user_object = sqlx::query!( - " - SELECT id, name - FROM users - WHERE id = $1 - " - , id - - ) - .fetch_one(&pool) - .await?; users.push( - Member { - id: user_object.id, - name: user_object.name - } + sqlx::query_as!( + Member, + " + SELECT id, name + FROM users + WHERE id = $1 + " + , id + ) + .fetch_one(&pool) + .await? ) } Ok(users) } -pub async fn update_organisation_admins(organisation_id: i64, admin_id_list: Vec, pool: Pool) -> Result { +pub async fn update_organisation_admins(organisation_id: i64, admin_id_list: Vec, pool: Pool) -> Result<(), Error> { sqlx::query!( - "DELETE FROM organisation_admins WHERE organisation_id = $1", + " + UPDATE organisations + SET organisation_admins = $1 + WHERE id = $2 + ", + &admin_ids, organisation_id ) - .execute(&pool) + .execute(pool) .await?; - - for admin_id in admin_id_list { - sqlx::query!( - " - INSERT INTO organisation_admins (organisation_id, user_id) - VALUES ($1, $2) - ", - organisation_id, - admin_id - ) - .execute(&pool) - .await?; - } - - Ok("Successfully updated members.".to_string()) + + Ok(()) } -pub async fn remove_admin_from_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result { - let new_organisation_admin = sqlx::query! - ( +pub async fn remove_admin_from_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result<(), Error> { + sqlx::query!( " - DELETE FROM organisation_admins - WHERE organisation_id = $1 - AND user_id = $2 - RETURNING * + UPDATE organisations + SET organisation_admins = array_remove(organisation_admins, $1) + WHERE id = $2 ", - organisation_id, - admin_id - ).fetch_one(&pool).await; - - match new_organisation_admin { - Ok(_) => { - println!("{:?}", new_organisation_admin); - Ok("Deleted organisation admin in database".to_string()) - }, - Err(_) => bail!("error: failed to remove admin from organisation") - } + admin_id, + organisation_id + ) + .execute(pool) + .await?; + + Ok(()) } - +// This is only here as a placeholder - replace when campaign is done #[derive(Deserialize, Serialize)] pub struct Campaign { // Define your struct fields based on the Campaign model @@ -232,42 +159,38 @@ pub struct Campaign { pub name: String, pub cover_image: Option, pub description: String, - pub starts_at: DateTime, - pub ends_at: DateTime, + pub starts_at: NaiveDateTime, + pub ends_at: NaiveDateTime } + +impl Campaign { + fn into_utc(self) -> Self { + Self { + starts_at: DateTime::from_utc(self.starts_at, Utc), + ends_at: DateTime::from_utc(self.ends_at, Utc), + ..self + } + } +} + pub async fn get_organisation_campaigns(id: i64, pool: Pool) -> Result> { - let campaigns = sqlx::query! - ( + let campaigns = sqlx::query_as!( + Campaign, " - SELECT * - FROM campaigns - WHERE organisation_id = $1 + SELECT id, name, cover_image, description, starts_at, ends_at + FROM campaigns + WHERE organisation_id = $1 ", id - ).fetch_all(&pool).await; + ) + .fetch_all(&pool) + .await?; - let mut result: Vec = Vec::new(); - match campaigns { - Ok(campaign_list) => { - for campaign in campaign_list { - let details = Campaign { - id: campaign.id, - name: campaign.name, - cover_image: campaign.cover_image, - description: campaign.description, - starts_at: campaign.starts_at.and_utc(), - ends_at: campaign.ends_at.and_utc() - }; - result.push(details); - } - }, - Err(_) => bail!("Error getting organisation campaign") - } - Ok(result) + Ok(campaigns) } -pub async fn create_campaign_for_organisation(id: i64, name: String, description: String, starts_at: NaiveDateTime, ends_at: NaiveDateTime, pool: Pool) -> Result { - let new_campaign = sqlx::query! +pub async fn create_campaign_for_organisation(id: i64, name: String, description: String, starts_at: NaiveDateTime, ends_at: NaiveDateTime, pool: Pool) -> Result<(), Error> { + sqlx::query! ( " INSERT INTO campaigns (id, name, description, starts_at, ends_at) @@ -279,21 +202,7 @@ pub async fn create_campaign_for_organisation(id: i64, name: String, description description, starts_at, ends_at - ).fetch_one(&pool).await; + ).fetch_one(&pool).await?; - match new_campaign { - Ok(res) => { - Ok( - Campaign { - id: res.id, - name: res.name, - cover_image: None, - description: res.description, - starts_at: res.starts_at.and_utc(), - ends_at: res.ends_at.and_utc() - } - ) - }, - Err(_) => bail!("error: failed to create campaign") - } + Ok(()) } \ No newline at end of file From 2068919f387801cdc3b87a63454da0f5a4ad0a60 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 7 Jun 2024 18:03:33 +1000 Subject: [PATCH 15/36] Update rust.yml to include 224 branch --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2bfbc151..fe8c05d7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,7 +2,7 @@ name: Rust on: pull_request: - branches: [main, "renovate/*"] + branches: [main, "renovate/*", "CHAOS-224-KHAOS-rewrite"] push: branches: ["renovate/*"] From 5604d74e696d017a3a9a40b2e666f69528320894 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 11:49:20 +0530 Subject: [PATCH 16/36] logic and style fixes --- backend/server/src/handler/mod.rs | 2 +- backend/server/src/handler/organisation.rs | 106 ++++----- backend/server/src/main.rs | 34 +-- backend/server/src/models/mod.rs | 2 +- backend/server/src/models/organisation.rs | 35 ++- backend/server/src/service/mod.rs | 2 +- backend/server/src/service/organisation.rs | 236 +++++++++++++-------- 7 files changed, 257 insertions(+), 160 deletions(-) diff --git a/backend/server/src/handler/mod.rs b/backend/server/src/handler/mod.rs index af037dc1..163c4759 100644 --- a/backend/server/src/handler/mod.rs +++ b/backend/server/src/handler/mod.rs @@ -1,2 +1,2 @@ pub mod auth; -pub mod organisation; \ No newline at end of file +pub mod organisation; diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 44a998c8..20856bf9 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,27 +1,18 @@ +use crate::models; use crate::models::app::AppState; use crate::models::auth::AuthUser; use crate::models::auth::SuperUser; -use axum::http::StatusCode; -use crate::models; +use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation}; use crate::service; -use axum::extract::{Path, State, Json}; +use axum::extract::{Json, Path, State}; +use axum::http::StatusCode; use axum::response::IntoResponse; -pub async fn get_organisations( - State(state): State, - _user: AuthUser -) -> Result { - match service::organisation::get_organisations(state.db).await { - Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), - } -} - pub async fn get_organisation( State(state): State, - Path(organisation_id): Path + Path(organisation_id): Path, ) -> Result { - match service::organisation::get_organisation(organisation_id, state.db).await { + match service::organisation::get_organisation(organisation_id, &state.db).await { Ok(organisation_details) => Ok((StatusCode::OK, Json(organisation_details))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -30,30 +21,22 @@ pub async fn get_organisation( pub async fn update_organisation_logo( State(state): State, Path(organisation_id): Path, - _user: SuperUser, - Json(request_body): Json, + user: AuthUser, ) -> Result { - match request_body.logo { - Some(logo) => { - // Logo exists, proceed with the update - match service::organisation::update_organisation_logo(organisation_id, logo, state.db).await { - Ok(logo_url) => Ok((StatusCode::OK, Json(logo_url))), - Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), - } - } - None => { - // Logo is missing, return an error - Err((StatusCode::BAD_REQUEST, "Logo is missing".to_string())) - } + match service::organisation::update_organisation_logo(organisation_id, user.user_id, &state.db) + .await + { + Ok(logo_url) => Ok((StatusCode::OK, Json(logo_url))), + Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } pub async fn delete_organisation( State(state): State, Path(organisation_id): Path, - _user: SuperUser + _user: SuperUser, ) -> Result { - match service::organisation::delete_organisation(organisation_id, _user.user_id, state.db).await { + match service::organisation::delete_organisation(organisation_id, &state.db).await { Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), Err(e) => return Err((StatusCode::UNAUTHORIZED, e.to_string())), } @@ -62,12 +45,16 @@ pub async fn delete_organisation( pub async fn create_organisation( State(state): State, _user: SuperUser, - Json(request_body): Json + Json(data): Json, ) -> Result { - let mut snowflake_generator = state.snowflake_generator; - let new_organisation_id = snowflake_generator.real_time_generate(); - - match service::organisation::create_organisation(new_organisation_id, request_body.name, state.db).await { + match service::organisation::create_organisation( + data.admin, + data.name, + state.snowflake_generator, + &state.db, + ) + .await + { Ok(message) => Ok((StatusCode::OK, Json(message))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -76,24 +63,28 @@ pub async fn create_organisation( pub async fn get_organisation_admins( State(state): State, Path(organisation_id): Path, - _user: SuperUser + user: AuthUser, ) -> Result { - match service::organisation::get_organisation_admins(organisation_id, state.db).await { + match service::organisation::get_organisation_members(organisation_id, state.db).await { Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } } -pub struct IdList { - members: Vec -} pub async fn update_organisation_admins( State(state): State, Path(organisation_id): Path, - Json(request_body): Json, - _user: SuperUser + Json(request_body): Json, + user: SuperUser, ) -> Result { - match service::organisation::update_organisation_admins(organisation_id, request_body.members, state.db).await { + match service::organisation::update_organisation_admins( + organisation_id, + user.user_id, + request_body.members, + &state.db, + ) + .await + { Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -102,9 +93,17 @@ pub async fn update_organisation_admins( pub async fn remove_admin_from_organisation( State(state): State, Path(organisation_id): Path, - _user: AuthUser + user: AuthUser, + Json(request_body): Json, ) -> Result { - match service::organisation::remove_admin_from_organisation(organisation_id, _user.user_id, state.db).await { + match service::organisation::remove_admin_from_organisation( + organisation_id, + user.user_id, + request_body.user_id, + &state.db, + ) + .await + { Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -123,13 +122,22 @@ pub async fn get_organisation_campaigns( pub async fn create_campaign_for_organisation( State(state): State, _user: SuperUser, - Json(request_body): Json + Json(request_body): Json, ) -> Result { let mut snowflake_generator = state.snowflake_generator; let new_campaign_id = snowflake_generator.real_time_generate(); - match service::organisation::create_campaign_for_organisation(new_campaign_id, request_body.name, request_body.description, request_body.starts_at, request_body.ends_at, state.db).await { + match service::organisation::create_campaign_for_organisation( + new_campaign_id, + request_body.name, + request_body.description, + request_body.starts_at, + request_body.ends_at, + state.db, + ) + .await + { Ok(message) => Ok((StatusCode::OK, Json(message))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } -} \ No newline at end of file +} diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index f2ecbce8..d1340c09 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,10 +1,12 @@ use anyhow::Result; -use axum::{routing::get, Router, routing::post}; +use axum::routing::patch; +use axum::{routing::get, routing::post, Router}; use jsonwebtoken::{DecodingKey, EncodingKey}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use std::env; + mod handler; mod models; mod service; @@ -48,25 +50,31 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) - .route("/api/v1/organisations", - post(handler::organisation::create_organisation) + .route( + "/api/v1/organisation", + post(handler::organisation::create_organisation), ) - .route("/api/v1/organisations/:organisation_id", + .route( + "/api/v1/organisation/:organisation_id", get(handler::organisation::get_organisation) - .delete(handler::organisation::delete_organisation) + .delete(handler::organisation::delete_organisation), ) - .route("/api/v1/organisations/:organisation_id/campaigns", - get(handler::organisation::get_organisation_campaigns) + .route( + "/api/v1/organisation/:organisation_id/campaigns", + get(handler::organisation::get_organisation_campaigns), ) - .route("/api/v1/organisations/:organisation_id/logo", - post(handler::organisation::update_organisation_logo) + .route( + "/api/v1/organisation/:organisation_id/logo", + patch(handler::organisation::update_organisation_logo), ) - .route("/api/v1/organisations/:organisation_id/members", + .route( + "/api/v1/organisation/:organisation_id/members", get(handler::organisation::get_organisation_admins) - .put(handler::organisation::update_organisation_admins) + .put(handler::organisation::update_organisation_admins), ) - .route("/api/v1/organisations/:organisation_id/campaign", - post(handler::organisation::create_campaign_for_organisation) + .route( + "/api/v1/organisation/:organisation_id/campaign", + post(handler::organisation::create_campaign_for_organisation), ) .with_state(state); diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index 2879f9cd..574f7f1b 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -1,4 +1,4 @@ pub mod app; pub mod auth; +pub mod organisation; pub mod user; -pub mod organisation; \ No newline at end of file diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index ee8206f5..2a2ee445 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -1,6 +1,6 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; -use chrono::{DateTime, Utc}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Organisation { @@ -9,8 +9,14 @@ pub struct Organisation { pub logo: Option, pub created_at: DateTime, pub updated_at: DateTime, - pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done - pub organisation_admins: Vec + pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done + pub organisation_admins: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct NewOrganisation { + pub name: String, + pub admin: i64, } #[derive(Deserialize, Serialize)] @@ -18,5 +24,26 @@ pub struct OrganisationDetails { pub id: i64, pub name: String, pub logo: Option, - pub created_at: DateTime + pub created_at: DateTime, +} + +#[derive(Deserialize, Serialize, FromRow)] +pub struct Member { + pub id: i64, + pub name: String, +} + +#[derive(Deserialize, Serialize)] +pub struct MemberList { + pub members: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct AdminUpdateList { + pub members: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct AdminToRemove { + pub user_id: i64, } diff --git a/backend/server/src/service/mod.rs b/backend/server/src/service/mod.rs index df48d803..5e708b6b 100644 --- a/backend/server/src/service/mod.rs +++ b/backend/server/src/service/mod.rs @@ -1,4 +1,4 @@ pub mod auth; pub mod jwt; pub mod oauth2; -pub mod organisation; \ No newline at end of file +pub mod organisation; diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index dc682369..a6467dac 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,152 +1,198 @@ use anyhow::{bail, Error, Result}; -use sqlx::{Pool, Postgres}; use chrono::{DateTime, Local, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; +use snowflake::SnowflakeIdGenerator; +use sqlx::{Pool, Postgres}; +use uuid::Uuid; + +use crate::models::organisation::{Member, MemberList, OrganisationDetails}; + +pub async fn is_admin(user_id: i64, organisation_id: i64, pool: &Pool) -> Result<()> { + let is_admin = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM organisation_admins WHERE organisation_id = $1 AND user_id = $2)", + organisation_id, + user_id + ) + .fetch_one(&pool) + .await?.exists.unwrap(); + + if !is_admin { + bail!("User is not an admin of organisation"); + } -use crate::models::organisation::OrganisationDetails; + Ok(()) +} -pub async fn get_organisation(id: i64, pool: Pool) -> Result { - let organisation_details = sqlx::query! - ( +pub async fn get_organisation(id: i64, pool: &Pool) -> Result { + let response = sqlx::query_as!( + OrganisationDetails, " - SELECT * + SELECT id, name, logo, created_at FROM organisations WHERE id = $1 ", - id).fetch_optional(&pool) - .await?; + id + ) + .fetch_optional(&pool) + .await?; - if let Some(result) = organisation_details { - let details = OrganisationDetails { - id: result.id, - name: result.name, - logo: result.logo, - created_at: result.created_at.and_utc() - }; + if let Some(details) = response { return Ok(details); } bail!("error: failed to get organisation"); } +pub async fn update_organisation_logo( + id: i64, + user_id: i64, + pool: &Pool, +) -> Result { + is_admin(user_id, id, pool).await?; + + let dt = Utc::now(); -pub async fn update_organisation_logo(id: i64, logo: String, pool: Pool) -> Result<()> { - let dt = Local::now(); - let current_time = dt.naive_utc(); - sqlx::query! - ( + let logo_id = Uuid::new_v4().to_string(); // TODO: Change db type to UUID + let current_time = dt; + sqlx::query!( " UPDATE organisations SET logo = $2, updated_at = $3 WHERE id = $1 ", id, - logo, + logo_id, current_time - ).fetch_one(&pool) + ) + .execute(&pool) .await?; - Ok(()) + // TODO: Generate a s3 url + let upload_url = "GENERATE AN S3 PRESIGNED URL".to_string(); + + Ok(upload_url) } -pub async fn delete_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result<()> { - // Delete organisation - sqlx::query! - ( +pub async fn delete_organisation(organisation_id: i64, pool: &Pool) -> Result<()> { + // Delete organisation + sqlx::query!( " - CASCADE DELETE FROM organisations WHERE id = $1 - RETURNING * + DELETE FROM organisations WHERE id = $1 ", organisation_id - ).fetch_one(&pool) + ) + .execute(&pool) .await?; Ok(()) } -pub async fn create_organisation(id: i64, name: String, pool: Pool) -> Result<(), Error> { - sqlx::query! - ( +pub async fn create_organisation( + admin_id: i64, + name: String, + mut snowflake_generator: SnowflakeIdGenerator, + pool: &Pool, +) -> Result<()> { + let id = snowflake_generator.generate(); + + sqlx::query!( " INSERT INTO organisations (id, name) VALUES ($1, $2) ", id, name - ).fetch_one(&pool) + ) + .execute(&pool) .await?; - - Ok(()) -} -// Below returns subset of user - not completed yet - remove comment when done -pub async fn get_organisation_admins(organisation_id: i64, pool: Pool) -> Result> { - let admin_list = sqlx::query! - ( - " - SELECT organisation_admins - FROM organisations - WHERE id = $1 + sqlx::query!( " - , organisation_id - ).fetch_all(&pool).await?; + INSERT INTO organisation_admins (organisation_id, user_id) + VALUES ($1, $2) + ", + id, + admin_id + ) + .execute(&pool) + .await?; - let admin_id_list: Vec = admin_list.into_iter().map(|row| row.user_id).collect(); + Ok(()) +} - if admin_id_list.is_empty() { - return Ok(Vec::new()); - } +pub async fn get_organisation_members( + organisation_id: i64, + user_id: i64, + pool: &Pool, +) -> Result { + is_admin(user_id, organisation_id, pool).await?; - let mut users = Vec::new(); - - for id in admin_id_list { - users.push( - sqlx::query_as!( - Member, - " - SELECT id, name - FROM users - WHERE id = $1 - " - , id - ) - .fetch_one(&pool) - .await? - ) - } + let admin_list = sqlx::query_as!( + Member, + " + SELECT organisation_admins.user_id as id, users.name from organisation_admins + LEFT JOIN users on users.id = organisation_admins.user_id + WHERE organisation_id = $1 + ", + organisation_id + ) + .fetch_all(&pool) + .await?; - Ok(users) + Ok(MemberList { + members: admin_list, + }) } -pub async fn update_organisation_admins(organisation_id: i64, admin_id_list: Vec, pool: Pool) -> Result<(), Error> { +pub async fn update_organisation_admins( + organisation_id: i64, + user_id: i64, + admin_id_list: Vec, + pool: &Pool, +) -> Result<()> { + is_admin(user_id, organisation_id, pool).await?; + sqlx::query!( - " - UPDATE organisations - SET organisation_admins = $1 - WHERE id = $2 - ", - &admin_ids, + "DELETE FROM organisation_admins WHERE organisation_id = $1", organisation_id ) - .execute(pool) + .execute(&pool) .await?; - + + for admin_id in admin_id_list { + sqlx::query!( + " + INSERT INTO organisation_admins (organisation_id, user_id) + VALUES ($1, $2) + ", + organisation_id, + admin_id + ) + .execute(&pool) + .await?; + } + Ok(()) } -pub async fn remove_admin_from_organisation(organisation_id: i64, admin_id: i64, pool: Pool) -> Result<(), Error> { +pub async fn remove_admin_from_organisation( + organisation_id: i64, + user_id: i64, + admin_to_remove: i64, + pool: &Pool, +) -> Result<()> { + is_admin(user_id, organisation_id, pool).await?; + sqlx::query!( " - UPDATE organisations - SET organisation_admins = array_remove(organisation_admins, $1) - WHERE id = $2 + DELETE FROM organisation_admins WHERE user_id = $1 AND organisation_id = $2 ", - admin_id, + admin_to_remove, organisation_id ) - .execute(pool) + .execute(&pool) .await?; - + Ok(()) } @@ -160,7 +206,7 @@ pub struct Campaign { pub cover_image: Option, pub description: String, pub starts_at: NaiveDateTime, - pub ends_at: NaiveDateTime + pub ends_at: NaiveDateTime, } impl Campaign { @@ -189,9 +235,15 @@ pub async fn get_organisation_campaigns(id: i64, pool: Pool) -> Result Ok(campaigns) } -pub async fn create_campaign_for_organisation(id: i64, name: String, description: String, starts_at: NaiveDateTime, ends_at: NaiveDateTime, pool: Pool) -> Result<(), Error> { - sqlx::query! - ( +pub async fn create_campaign_for_organisation( + id: i64, + name: String, + description: String, + starts_at: NaiveDateTime, + ends_at: NaiveDateTime, + pool: Pool, +) -> Result<(), Error> { + sqlx::query!( " INSERT INTO campaigns (id, name, description, starts_at, ends_at) VALUES ($1, $2, $3, $4, $5) @@ -202,7 +254,9 @@ pub async fn create_campaign_for_organisation(id: i64, name: String, description description, starts_at, ends_at - ).fetch_one(&pool).await?; + ) + .fetch_one(&pool) + .await?; Ok(()) -} \ No newline at end of file +} From 4236b273296b13e4fc4d65e69d4dcd8cc21fd69f Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 17:22:08 +0530 Subject: [PATCH 17/36] Change to using `thiserror` --- backend/server/Cargo.toml | 1 + backend/server/src/models/auth.rs | 14 ++++----- backend/server/src/models/error.rs | 47 ++++++++++++++++++------------ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index fd764748..84445fdc 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -14,6 +14,7 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "time" # Important secondary crates anyhow = "1.0" +thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } reqwest = { version = "0.11", features = ["json"] } serde_json = "1.0" diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 149c3a25..1e164200 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -52,12 +52,12 @@ where let TypedHeader(cookies) = parts .extract::>() .await - .map_err(|_| ChaosError::NotLoggedIn)?; + .map_err(|_| ChaosError::NotLoggedInError)?; - let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedInError)?; let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedInError)?; Ok(AuthUser { user_id: claims.sub, @@ -85,12 +85,12 @@ where let TypedHeader(cookies) = parts .extract::>() .await - .map_err(|_| ChaosError::NotLoggedIn)?; + .map_err(|_| ChaosError::NotLoggedInError)?; - let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedIn)?; + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedInError)?; let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedInError)?; let pool = &app_state.db; let possible_user = is_super_user(claims.sub, pool).await; @@ -102,6 +102,6 @@ where } } - Err(ChaosError::NotLoggedIn) + Err(ChaosError::UnauthorizedError) } } diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index ca160d97..67aaa001 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -3,31 +3,42 @@ use axum::response::{IntoResponse, Redirect, Response}; /// Custom error enum for Chaos. /// -/// Handles all anyhow errors (when `?` is used) alongside +/// Handles all errors thrown by libraries (when `?` is used) alongside /// specific errors for business logic. +#[derive(thiserror::Error, Debug)] pub enum ChaosError { - NotLoggedIn, - Unauthorized, - ForbiddenOperation, - ServerError(anyhow::Error), + #[error("Not logged in")] + NotLoggedInError, + + #[error("Not authorized")] + UnauthorizedError, + + #[error("Forbidden operation")] + ForbiddenOperationError, + + #[error("SQLx error")] + DatabaseError(#[from] sqlx::Error), + + #[error("Reqwest error")] + ReqwestError(#[from] reqwest::Error), + + #[error("OAuth2 error")] + OAuthError(#[from] oauth2::RequestTokenError, oauth2::StandardErrorResponse>) } +/// Implementation for converting errors into responses. Manages error code and message returned. impl IntoResponse for ChaosError { fn into_response(self) -> Response { match self { - ChaosError::NotLoggedIn => Redirect::temporary("/auth/google").into_response(), - ChaosError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), - ChaosError::ForbiddenOperation => (StatusCode::FORBIDDEN, "Forbidden operation").into_response(), - ChaosError::ServerError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + ChaosError::NotLoggedInError => Redirect::temporary("/auth/google").into_response(), + ChaosError::UnauthorizedError => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), + ChaosError::ForbiddenOperationError => (StatusCode::FORBIDDEN, "Forbidden operation").into_response(), + ChaosError::DatabaseError(db_error) => match db_error { + // We only care about the RowNotFound error, as others are miscellaneous DB errors. + sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Not found").into_response(), + _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response(), + }, + _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() } } } - -impl From for ChaosError -where - E: Into, -{ - fn from(err: E) -> Self { - ChaosError::ServerError(err.into()) - } -} From f0bd6770b50411ddcac3c8f4dc518a4b9bab9631 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 17:48:46 +0530 Subject: [PATCH 18/36] add organisation_role type to db --- backend/migrations/20240621120853_organisation_roles.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 backend/migrations/20240621120853_organisation_roles.sql diff --git a/backend/migrations/20240621120853_organisation_roles.sql b/backend/migrations/20240621120853_organisation_roles.sql new file mode 100644 index 00000000..239d279f --- /dev/null +++ b/backend/migrations/20240621120853_organisation_roles.sql @@ -0,0 +1,4 @@ +CREATE TYPE organisation_role AS ENUM ('User', 'Admin'); + +ALTER TABLE organisation_admins RENAME TO organisation_members; +ALTER TABLE organisation_members ADD COLUMN role organisation_role DEFAULT 'User' NOT NULL; \ No newline at end of file From 7468b161731d1993dbc6261d65567d411b05c4b7 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 18:12:05 +0530 Subject: [PATCH 19/36] update migration timestamps to be `NOT NULL` --- backend/migrations/20240406023149_create_users.sql | 4 ++-- .../20240406024211_create_organisations.sql | 13 ++++++++----- .../migrations/20240406025537_create_campaigns.sql | 8 ++++---- .../migrations/20240406031400_create_questions.sql | 4 ++-- .../20240406031915_create_applications.sql | 8 ++++---- .../20240621120853_organisation_roles.sql | 4 ---- 6 files changed, 20 insertions(+), 21 deletions(-) delete mode 100644 backend/migrations/20240621120853_organisation_roles.sql diff --git a/backend/migrations/20240406023149_create_users.sql b/backend/migrations/20240406023149_create_users.sql index 0bb4cd8b..f862c0da 100644 --- a/backend/migrations/20240406023149_create_users.sql +++ b/backend/migrations/20240406023149_create_users.sql @@ -8,8 +8,8 @@ CREATE TABLE users ( degree_name TEXT, degree_starting_year INTEGER, role user_role NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL ); CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email))); diff --git a/backend/migrations/20240406024211_create_organisations.sql b/backend/migrations/20240406024211_create_organisations.sql index 88dda366..2e9c8bf7 100644 --- a/backend/migrations/20240406024211_create_organisations.sql +++ b/backend/migrations/20240406024211_create_organisations.sql @@ -2,19 +2,22 @@ CREATE TABLE organisations ( id BIGINT PRIMARY KEY, name TEXT NOT NULL UNIQUE, logo TEXT, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL ); -CREATE TABLE organisation_admins ( +CREATE TYPE organisation_role AS ENUM ('User', 'Admin'); + +CREATE TABLE organisation_members ( id SERIAL PRIMARY KEY, organisation_id BIGINT NOT NULL, user_id BIGINT NOT NULL, - CONSTRAINT FK_organisation_admins_organisation + role organisation_role DEFAULT 'User' NOT NULL, + CONSTRAINT FK_organisation_members_organisation FOREIGN KEY(organisation_id) REFERENCES organisations(id) ON DELETE CASCADE ON UPDATE CASCADE ); -CREATE INDEX IDX_organisation_admins_organisation on organisation_admins (organisation_id); +CREATE INDEX IDX_organisation_admins_organisation on organisation_members (organisation_id); diff --git a/backend/migrations/20240406025537_create_campaigns.sql b/backend/migrations/20240406025537_create_campaigns.sql index 46bca9df..c49df429 100644 --- a/backend/migrations/20240406025537_create_campaigns.sql +++ b/backend/migrations/20240406025537_create_campaigns.sql @@ -6,8 +6,8 @@ CREATE TABLE campaigns ( description TEXT, starts_at TIMESTAMPTZ NOT NULL, ends_at TIMESTAMPTZ NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_campaigns_organisations FOREIGN KEY(organisation_id) REFERENCES organisations(id) @@ -23,8 +23,8 @@ CREATE TABLE campaign_roles ( min_available INTEGER, max_available INTEGER, finalised BOOLEAN, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_campaign_roles_campaign FOREIGN KEY(campaign_id) REFERENCES campaigns(id) diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql index fa170813..050d07fe 100644 --- a/backend/migrations/20240406031400_create_questions.sql +++ b/backend/migrations/20240406031400_create_questions.sql @@ -7,8 +7,8 @@ CREATE TABLE questions ( required BOOLEAN, question_type question_type NOT NULL, campaign_id BIGINT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_questions_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index 91d9edcf..767abb92 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -6,8 +6,8 @@ CREATE TABLE applications ( user_id BIGINT NOT NULL, status application_status NOT NULL, private_status application_status NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_applications_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) @@ -95,8 +95,8 @@ CREATE TABLE application_ratings ( application_id BIGINT NOT NULL, rater_id BIGINT NOT NULL, rating INTEGER NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_application_ratings_applications FOREIGN KEY(application_id) REFERENCES applications(id) diff --git a/backend/migrations/20240621120853_organisation_roles.sql b/backend/migrations/20240621120853_organisation_roles.sql deleted file mode 100644 index 239d279f..00000000 --- a/backend/migrations/20240621120853_organisation_roles.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TYPE organisation_role AS ENUM ('User', 'Admin'); - -ALTER TABLE organisation_admins RENAME TO organisation_members; -ALTER TABLE organisation_members ADD COLUMN role organisation_role DEFAULT 'User' NOT NULL; \ No newline at end of file From d7dffef87fa19b78a44d7a4a8f83cf30f293967a Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 18:12:17 +0530 Subject: [PATCH 20/36] change sqlx `time` to `chrono` --- backend/server/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 84445fdc..6f556cdd 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" tokio = { version = "1.34", features = ["macros", "rt-multi-thread"] } axum = { version = "0.7", features = ["macros"] } axum-extra = { version = "0.9", features = ["typed-header"] } -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] } # Important secondary crates anyhow = "1.0" @@ -25,3 +25,4 @@ uuid = { version = "1.5", features = ["serde", "v4"] } rs-snowflake = "0.6" jsonwebtoken = "9.1" dotenvy = "0.15" + From d52f3e4859f848f79c7f9bcfd2265c81af9fe2f5 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 18:12:32 +0530 Subject: [PATCH 21/36] integrate organisations crud with error handling --- backend/server/src/handler/organisation.rs | 19 +-- backend/server/src/models/campaign.rs | 12 ++ backend/server/src/models/error.rs | 2 +- backend/server/src/models/mod.rs | 2 + backend/server/src/models/organisation.rs | 10 +- backend/server/src/service/organisation.rs | 138 ++++++++------------- 6 files changed, 89 insertions(+), 94 deletions(-) create mode 100644 backend/server/src/models/campaign.rs diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 20856bf9..38d2b3d9 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -7,15 +7,14 @@ use crate::service; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; +use crate::models::error::ChaosError; pub async fn get_organisation( State(state): State, Path(organisation_id): Path, -) -> Result { - match service::organisation::get_organisation(organisation_id, &state.db).await { - Ok(organisation_details) => Ok((StatusCode::OK, Json(organisation_details))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), - } +) -> Result { + let org = service::organisation::get_organisation(organisation_id, &state.db).await?; + Ok((StatusCode::OK, Json(org))) } pub async fn update_organisation_logo( @@ -65,7 +64,7 @@ pub async fn get_organisation_admins( Path(organisation_id): Path, user: AuthUser, ) -> Result { - match service::organisation::get_organisation_members(organisation_id, state.db).await { + match service::organisation::get_organisation_members(organisation_id, user.user_id, &state.db).await { Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), } @@ -113,9 +112,11 @@ pub async fn get_organisation_campaigns( State(state): State, Path(organisation_id): Path, ) -> Result { - match service::organisation::get_organisation_campaigns(organisation_id, state.db).await { + match service::organisation::get_organisation_campaigns(organisation_id, &state.db).await { Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), + Err(e) => { + return Err((StatusCode::NOT_FOUND, e.to_string())) + }, } } @@ -133,7 +134,7 @@ pub async fn create_campaign_for_organisation( request_body.description, request_body.starts_at, request_body.ends_at, - state.db, + &state.db, ) .await { diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs new file mode 100644 index 00000000..ce422ec5 --- /dev/null +++ b/backend/server/src/models/campaign.rs @@ -0,0 +1,12 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Campaign { + pub id: i64, + pub name: String, + pub description: Option, + pub cover_image: Option, + pub starts_at: DateTime, + pub ends_at: DateTime, +} \ No newline at end of file diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 67aaa001..390fa478 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -41,4 +41,4 @@ impl IntoResponse for ChaosError { _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() } } -} +} \ No newline at end of file diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index b992e0f9..c98b769a 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -2,3 +2,5 @@ pub mod app; pub mod auth; pub mod error; pub mod user; +pub mod campaign; +pub mod organisation; diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 2a2ee445..7e5df1a1 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use crate::models::campaign::Campaign; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Organisation { @@ -27,6 +28,13 @@ pub struct OrganisationDetails { pub created_at: DateTime, } +#[derive(Deserialize, Serialize, sqlx::Type, Clone)] +#[sqlx(type_name = "OrganisationRole", rename_all = "PascalCase")] +pub enum OrganisationRole { + User, + Admin, +} + #[derive(Deserialize, Serialize, FromRow)] pub struct Member { pub id: i64, @@ -46,4 +54,4 @@ pub struct AdminUpdateList { #[derive(Deserialize, Serialize)] pub struct AdminToRemove { pub user_id: i64, -} +} \ No newline at end of file diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index a6467dac..61581c7d 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,30 +1,30 @@ -use anyhow::{bail, Error, Result}; use chrono::{DateTime, Local, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; use uuid::Uuid; +use crate::models::organisation::{Member, MemberList, OrganisationDetails, OrganisationRole}; +use crate::models::campaign::Campaign; +use crate::models::error::ChaosError; -use crate::models::organisation::{Member, MemberList, OrganisationDetails}; - -pub async fn is_admin(user_id: i64, organisation_id: i64, pool: &Pool) -> Result<()> { +pub async fn is_admin(user_id: i64, organisation_id: i64, pool: &Pool) -> Result<(), ChaosError> { let is_admin = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM organisation_admins WHERE organisation_id = $1 AND user_id = $2)", + "SELECT EXISTS(SELECT 1 FROM organisation_members WHERE organisation_id = $1 AND user_id = $2 AND role = 'Admin')", organisation_id, user_id ) - .fetch_one(&pool) + .fetch_one(pool) .await?.exists.unwrap(); if !is_admin { - bail!("User is not an admin of organisation"); + return Err(ChaosError::UnauthorizedError) } Ok(()) } -pub async fn get_organisation(id: i64, pool: &Pool) -> Result { - let response = sqlx::query_as!( +pub async fn get_organisation(id: i64, pool: &Pool) -> Result { + Ok(sqlx::query_as!( OrganisationDetails, " SELECT id, name, logo, created_at @@ -33,21 +33,15 @@ pub async fn get_organisation(id: i64, pool: &Pool) -> Result, -) -> Result { +) -> Result { is_admin(user_id, id, pool).await?; let dt = Utc::now(); @@ -64,7 +58,7 @@ pub async fn update_organisation_logo( logo_id, current_time ) - .execute(&pool) + .execute(pool) .await?; // TODO: Generate a s3 url @@ -73,15 +67,14 @@ pub async fn update_organisation_logo( Ok(upload_url) } -pub async fn delete_organisation(organisation_id: i64, pool: &Pool) -> Result<()> { - // Delete organisation +pub async fn delete_organisation(organisation_id: i64, pool: &Pool) -> Result<(), ChaosError> { sqlx::query!( " DELETE FROM organisations WHERE id = $1 ", organisation_id ) - .execute(&pool) + .execute(pool) .await?; Ok(()) @@ -92,7 +85,7 @@ pub async fn create_organisation( name: String, mut snowflake_generator: SnowflakeIdGenerator, pool: &Pool, -) -> Result<()> { +) -> Result<(), ChaosError> { let id = snowflake_generator.generate(); sqlx::query!( @@ -103,18 +96,19 @@ pub async fn create_organisation( id, name ) - .execute(&pool) + .execute(pool) .await?; sqlx::query!( " - INSERT INTO organisation_admins (organisation_id, user_id) - VALUES ($1, $2) + INSERT INTO organisation_members (organisation_id, user_id, role) + VALUES ($1, $2, $3) ", id, - admin_id + admin_id, + OrganisationRole::Admin as OrganisationRole ) - .execute(&pool) + .execute(pool) .await?; Ok(()) @@ -124,24 +118,26 @@ pub async fn get_organisation_members( organisation_id: i64, user_id: i64, pool: &Pool, -) -> Result { +) -> Result { is_admin(user_id, organisation_id, pool).await?; let admin_list = sqlx::query_as!( Member, " - SELECT organisation_admins.user_id as id, users.name from organisation_admins - LEFT JOIN users on users.id = organisation_admins.user_id + SELECT organisation_members.user_id as id, users.name from organisation_members + LEFT JOIN users on users.id = organisation_members.user_id WHERE organisation_id = $1 ", organisation_id ) - .fetch_all(&pool) + .fetch_all(pool) .await?; - Ok(MemberList { - members: admin_list, - }) + Ok( + MemberList { + members: admin_list, + } + ) } pub async fn update_organisation_admins( @@ -149,26 +145,28 @@ pub async fn update_organisation_admins( user_id: i64, admin_id_list: Vec, pool: &Pool, -) -> Result<()> { +) -> Result<(), ChaosError> { is_admin(user_id, organisation_id, pool).await?; sqlx::query!( - "DELETE FROM organisation_admins WHERE organisation_id = $1", - organisation_id + "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", + organisation_id, + OrganisationRole::Admin as OrganisationRole ) - .execute(&pool) + .execute(pool) .await?; for admin_id in admin_id_list { sqlx::query!( " - INSERT INTO organisation_admins (organisation_id, user_id) - VALUES ($1, $2) - ", + INSERT INTO organisation_members (organisation_id, user_id, role) + VALUES ($1, $2, $3) + ", organisation_id, - admin_id + admin_id, + OrganisationRole::Admin as OrganisationRole ) - .execute(&pool) + .execute(pool) .await?; } @@ -180,47 +178,24 @@ pub async fn remove_admin_from_organisation( user_id: i64, admin_to_remove: i64, pool: &Pool, -) -> Result<()> { +) -> Result<(), ChaosError> { is_admin(user_id, organisation_id, pool).await?; sqlx::query!( " - DELETE FROM organisation_admins WHERE user_id = $1 AND organisation_id = $2 + DELETE FROM organisation_members WHERE user_id = $1 AND organisation_id = $2 ", admin_to_remove, organisation_id ) - .execute(&pool) + .execute(pool) .await?; Ok(()) } -// This is only here as a placeholder - replace when campaign is done -#[derive(Deserialize, Serialize)] -pub struct Campaign { - // Define your struct fields based on the Campaign model - // For example: - pub id: i64, - pub name: String, - pub cover_image: Option, - pub description: String, - pub starts_at: NaiveDateTime, - pub ends_at: NaiveDateTime, -} - -impl Campaign { - fn into_utc(self) -> Self { - Self { - starts_at: DateTime::from_utc(self.starts_at, Utc), - ends_at: DateTime::from_utc(self.ends_at, Utc), - ..self - } - } -} - -pub async fn get_organisation_campaigns(id: i64, pool: Pool) -> Result> { - let campaigns = sqlx::query_as!( +pub async fn get_organisation_campaigns(id: i64, pool: &Pool) -> Result, ChaosError> { + Ok(sqlx::query_as!( Campaign, " SELECT id, name, cover_image, description, starts_at, ends_at @@ -229,25 +204,22 @@ pub async fn get_organisation_campaigns(id: i64, pool: Pool) -> Result ", id ) - .fetch_all(&pool) - .await?; - - Ok(campaigns) + .fetch_all(pool) + .await?) } pub async fn create_campaign_for_organisation( id: i64, name: String, - description: String, - starts_at: NaiveDateTime, - ends_at: NaiveDateTime, - pool: Pool, -) -> Result<(), Error> { + description: Option, + starts_at: DateTime, + ends_at: DateTime, + pool: &Pool, +) -> Result<(), ChaosError> { sqlx::query!( " INSERT INTO campaigns (id, name, description, starts_at, ends_at) VALUES ($1, $2, $3, $4, $5) - RETURNING * ", id, name, @@ -255,7 +227,7 @@ pub async fn create_campaign_for_organisation( starts_at, ends_at ) - .fetch_one(&pool) + .execute(pool) .await?; Ok(()) From bcb6be82ba4971a1cf639504fb06c5e8cd490865 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 18:31:11 +0530 Subject: [PATCH 22/36] return member role with org members --- backend/api.json | 4 ++++ backend/api.yaml | 3 +++ backend/server/src/models/organisation.rs | 3 ++- backend/server/src/service/organisation.rs | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/api.json b/backend/api.json index f070ae82..8be962fe 100644 --- a/backend/api.json +++ b/backend/api.json @@ -746,6 +746,10 @@ "name": { "type": "string", "example": "Clancy Lion" + }, + "role": { + "type": "string", + "example": "Admin" } } } diff --git a/backend/api.yaml b/backend/api.yaml index 64be4694..0456733a 100644 --- a/backend/api.yaml +++ b/backend/api.yaml @@ -482,6 +482,9 @@ paths: name: type: string example: Clancy Lion + role: + type: string + example: Admin '403': description: User is not an organisation admin or member. content: diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 7e5df1a1..5dd7bfad 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -29,7 +29,7 @@ pub struct OrganisationDetails { } #[derive(Deserialize, Serialize, sqlx::Type, Clone)] -#[sqlx(type_name = "OrganisationRole", rename_all = "PascalCase")] +#[sqlx(type_name = "organisation_role", rename_all = "PascalCase")] pub enum OrganisationRole { User, Admin, @@ -39,6 +39,7 @@ pub enum OrganisationRole { pub struct Member { pub id: i64, pub name: String, + pub role: OrganisationRole, } #[derive(Deserialize, Serialize)] diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index 61581c7d..adb43e9e 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -124,7 +124,7 @@ pub async fn get_organisation_members( let admin_list = sqlx::query_as!( Member, " - SELECT organisation_members.user_id as id, users.name from organisation_members + SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members LEFT JOIN users on users.id = organisation_members.user_id WHERE organisation_id = $1 ", From 2b11ecca32ebd3a8bd0aa4b0d303728d953fb8e9 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 18:31:25 +0530 Subject: [PATCH 23/36] update sqlx type name for `UserRole` --- backend/server/src/models/user.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server/src/models/user.rs b/backend/server/src/models/user.rs index 1f99f52f..82c8f042 100644 --- a/backend/server/src/models/user.rs +++ b/backend/server/src/models/user.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, sqlx::Type, Clone)] -#[sqlx(type_name = "UserRole", rename_all = "PascalCase")] +#[sqlx(type_name = "user_role", rename_all = "PascalCase")] pub enum UserRole { User, SuperUser, From fdebc28f12caee0e2bbce9ebf6458531e945e8d0 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 18:31:39 +0530 Subject: [PATCH 24/36] simplify handlers to use new error type --- backend/server/src/handler/organisation.rs | 86 +++++++++------------- 1 file changed, 33 insertions(+), 53 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 38d2b3d9..9e47ef4e 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -21,53 +21,42 @@ pub async fn update_organisation_logo( State(state): State, Path(organisation_id): Path, user: AuthUser, -) -> Result { - match service::organisation::update_organisation_logo(organisation_id, user.user_id, &state.db) - .await - { - Ok(logo_url) => Ok((StatusCode::OK, Json(logo_url))), - Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), - } +) -> Result { + let logo_url = service::organisation::update_organisation_logo(organisation_id, user.user_id, &state.db).await?; + Ok((StatusCode::OK, Json(logo_url))) } pub async fn delete_organisation( State(state): State, Path(organisation_id): Path, _user: SuperUser, -) -> Result { - match service::organisation::delete_organisation(organisation_id, &state.db).await { - Ok(organisation) => Ok((StatusCode::OK, Json(organisation))), - Err(e) => return Err((StatusCode::UNAUTHORIZED, e.to_string())), - } +) -> Result { + service::organisation::delete_organisation(organisation_id, &state.db).await?; + Ok((StatusCode::OK, "Successfully deleted organisation")) } pub async fn create_organisation( State(state): State, _user: SuperUser, Json(data): Json, -) -> Result { - match service::organisation::create_organisation( +) -> Result { + service::organisation::create_organisation( data.admin, data.name, state.snowflake_generator, &state.db, ) - .await - { - Ok(message) => Ok((StatusCode::OK, Json(message))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), - } + .await?; + Ok((StatusCode::OK, "Successfully created organisation")) } -pub async fn get_organisation_admins( +pub async fn get_organisation_members( State(state): State, Path(organisation_id): Path, user: AuthUser, -) -> Result { - match service::organisation::get_organisation_members(organisation_id, user.user_id, &state.db).await { - Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), - } +) -> Result { + let members = service::organisation::get_organisation_members(organisation_id, user.user_id, &state.db).await?; + Ok((StatusCode::OK, Json(members))) } pub async fn update_organisation_admins( @@ -75,18 +64,16 @@ pub async fn update_organisation_admins( Path(organisation_id): Path, Json(request_body): Json, user: SuperUser, -) -> Result { - match service::organisation::update_organisation_admins( +) -> Result { + service::organisation::update_organisation_admins( organisation_id, user.user_id, request_body.members, &state.db, ) - .await - { - Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), - } + .await?; + + Ok((StatusCode::OK, "Successfully updated organisation admins")) } pub async fn remove_admin_from_organisation( @@ -94,41 +81,36 @@ pub async fn remove_admin_from_organisation( Path(organisation_id): Path, user: AuthUser, Json(request_body): Json, -) -> Result { - match service::organisation::remove_admin_from_organisation( +) -> Result { + service::organisation::remove_admin_from_organisation( organisation_id, user.user_id, request_body.user_id, &state.db, ) - .await - { - Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), - } + .await?; + + Ok((StatusCode::OK, "Successfully removed admin from organisation")) } pub async fn get_organisation_campaigns( State(state): State, Path(organisation_id): Path, -) -> Result { - match service::organisation::get_organisation_campaigns(organisation_id, &state.db).await { - Ok(organisation_admins) => Ok((StatusCode::OK, Json(organisation_admins))), - Err(e) => { - return Err((StatusCode::NOT_FOUND, e.to_string())) - }, - } +) -> Result { + let campaigns = service::organisation::get_organisation_campaigns(organisation_id, &state.db).await?; + + Ok((StatusCode::OK, Json(campaigns))) } pub async fn create_campaign_for_organisation( State(state): State, _user: SuperUser, Json(request_body): Json, -) -> Result { +) -> Result { let mut snowflake_generator = state.snowflake_generator; let new_campaign_id = snowflake_generator.real_time_generate(); - match service::organisation::create_campaign_for_organisation( + service::organisation::create_campaign_for_organisation( new_campaign_id, request_body.name, request_body.description, @@ -136,9 +118,7 @@ pub async fn create_campaign_for_organisation( request_body.ends_at, &state.db, ) - .await - { - Ok(message) => Ok((StatusCode::OK, Json(message))), - Err(e) => return Err((StatusCode::NOT_FOUND, e.to_string())), - } + .await?; + + Ok((StatusCode::OK, "Successfully created campaign")) } From 417e642786b6b22d524df1e391bbc9d973414f84 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 21 Jun 2024 18:32:26 +0530 Subject: [PATCH 25/36] removed unused imports --- backend/server/src/service/organisation.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index adb43e9e..b94bd437 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,5 +1,4 @@ -use chrono::{DateTime, Local, NaiveDateTime, Utc}; -use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; use uuid::Uuid; From fa88d4ef127b76a09e27fb4f469114e2ad98b52c Mon Sep 17 00:00:00 2001 From: Kavika Date: Wed, 26 Jun 2024 12:55:05 +1000 Subject: [PATCH 26/36] added `OrganisationAdmin` extractor --- backend/server/src/handler/organisation.rs | 36 +++++++++++------ backend/server/src/models/auth.rs | 47 +++++++++++++++++++++- backend/server/src/models/error.rs | 4 ++ backend/server/src/service/organisation.rs | 19 +++------ 4 files changed, 79 insertions(+), 27 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 9e47ef4e..19451c1b 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,6 +1,6 @@ use crate::models; use crate::models::app::AppState; -use crate::models::auth::AuthUser; +use crate::models::auth::{AuthUser, OrganisationAdmin}; use crate::models::auth::SuperUser; use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation}; use crate::service; @@ -20,9 +20,9 @@ pub async fn get_organisation( pub async fn update_organisation_logo( State(state): State, Path(organisation_id): Path, - user: AuthUser, + admin: OrganisationAdmin, ) -> Result { - let logo_url = service::organisation::update_organisation_logo(organisation_id, user.user_id, &state.db).await?; + let logo_url = service::organisation::update_organisation_logo(organisation_id, admin.user_id, &state.db).await?; Ok((StatusCode::OK, Json(logo_url))) } @@ -40,22 +40,32 @@ pub async fn create_organisation( _user: SuperUser, Json(data): Json, ) -> Result { - service::organisation::create_organisation( + let mut transaction = state.db.begin().await?; + + match service::organisation::create_organisation( data.admin, data.name, state.snowflake_generator, - &state.db, + &mut transaction, ) - .await?; - Ok((StatusCode::OK, "Successfully created organisation")) + .await { + Ok(..) => { + transaction.commit().await?; + Ok((StatusCode::OK, "Successfully created organisation")) + }, + Err(err) => { + transaction.rollback().await?; + Err(err) + } + } } pub async fn get_organisation_members( State(state): State, Path(organisation_id): Path, - user: AuthUser, + admin: OrganisationAdmin, ) -> Result { - let members = service::organisation::get_organisation_members(organisation_id, user.user_id, &state.db).await?; + let members = service::organisation::get_organisation_members(organisation_id, admin.user_id, &state.db).await?; Ok((StatusCode::OK, Json(members))) } @@ -63,11 +73,11 @@ pub async fn update_organisation_admins( State(state): State, Path(organisation_id): Path, Json(request_body): Json, - user: SuperUser, + admin: OrganisationAdmin, ) -> Result { service::organisation::update_organisation_admins( organisation_id, - user.user_id, + admin.user_id, request_body.members, &state.db, ) @@ -79,12 +89,12 @@ pub async fn update_organisation_admins( pub async fn remove_admin_from_organisation( State(state): State, Path(organisation_id): Path, - user: AuthUser, + admin: OrganisationAdmin, Json(request_body): Json, ) -> Result { service::organisation::remove_admin_from_organisation( organisation_id, - user.user_id, + admin.user_id, request_body.user_id, &state.db, ) diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 0227c623..5d839528 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::auth::is_super_user; use crate::service::jwt::decode_auth_token; -use axum::extract::{FromRef, FromRequestParts}; +use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; use axum::{ @@ -11,6 +11,7 @@ use axum::{ }; use axum_extra::{headers::Cookie, TypedHeader}; use serde::{Deserialize, Serialize}; +use crate::service::organisation::is_admin; #[derive(Deserialize, Serialize)] pub struct AuthRequest { @@ -106,3 +107,47 @@ where Err(ChaosError::UnauthorizedError) } } + +pub struct OrganisationAdmin { + pub user_id: i64, +} + +#[async_trait] +impl FromRequestParts for OrganisationAdmin + 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 decoding_key = &app_state.decoding_key; + let jwt_validator = &app_state.jwt_validator; + let TypedHeader(cookies) = parts + .extract::>() + .await + .map_err(|_| ChaosError::NotLoggedInError)?; + + let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedInError)?; + + let claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedInError)?; + + let pool = &app_state.db; + let user_id = claims.sub; + + let Path(organisation_id) = parts + .extract::>() + .await + .map_err(|_| ChaosError::BadRequest)?; + + is_admin(user_id, organisation_id, pool).await?; + + Ok( + OrganisationAdmin { + user_id + } + ) + } +} \ No newline at end of file diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 390fa478..533a4f4c 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -16,6 +16,9 @@ pub enum ChaosError { #[error("Forbidden operation")] ForbiddenOperationError, + #[error("Bad request")] + BadRequest, + #[error("SQLx error")] DatabaseError(#[from] sqlx::Error), @@ -33,6 +36,7 @@ impl IntoResponse for ChaosError { ChaosError::NotLoggedInError => Redirect::temporary("/auth/google").into_response(), ChaosError::UnauthorizedError => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), ChaosError::ForbiddenOperationError => (StatusCode::FORBIDDEN, "Forbidden operation").into_response(), + ChaosError::BadRequest => (StatusCode::BAD_REQUEST, "Bad request").into_response(), ChaosError::DatabaseError(db_error) => match db_error { // We only care about the RowNotFound error, as others are miscellaneous DB errors. sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Not found").into_response(), diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index b94bd437..b86deccc 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,6 +1,7 @@ +use std::ops::DerefMut; use chrono::{DateTime, Utc}; use snowflake::SnowflakeIdGenerator; -use sqlx::{Pool, Postgres}; +use sqlx::{Pool, Postgres, Transaction}; use uuid::Uuid; use crate::models::organisation::{Member, MemberList, OrganisationDetails, OrganisationRole}; use crate::models::campaign::Campaign; @@ -13,7 +14,7 @@ pub async fn is_admin(user_id: i64, organisation_id: i64, pool: &Pool) user_id ) .fetch_one(pool) - .await?.exists.unwrap(); + .await?.exists.expect("`exists` should always exist in this query result"); if !is_admin { return Err(ChaosError::UnauthorizedError) @@ -41,8 +42,6 @@ pub async fn update_organisation_logo( user_id: i64, pool: &Pool, ) -> Result { - is_admin(user_id, id, pool).await?; - let dt = Utc::now(); let logo_id = Uuid::new_v4().to_string(); // TODO: Change db type to UUID @@ -83,7 +82,7 @@ pub async fn create_organisation( admin_id: i64, name: String, mut snowflake_generator: SnowflakeIdGenerator, - pool: &Pool, + transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { let id = snowflake_generator.generate(); @@ -95,7 +94,7 @@ pub async fn create_organisation( id, name ) - .execute(pool) + .execute(transaction.deref_mut()) .await?; sqlx::query!( @@ -107,7 +106,7 @@ pub async fn create_organisation( admin_id, OrganisationRole::Admin as OrganisationRole ) - .execute(pool) + .execute(transaction.deref_mut()) .await?; Ok(()) @@ -118,8 +117,6 @@ pub async fn get_organisation_members( user_id: i64, pool: &Pool, ) -> Result { - is_admin(user_id, organisation_id, pool).await?; - let admin_list = sqlx::query_as!( Member, " @@ -145,8 +142,6 @@ pub async fn update_organisation_admins( admin_id_list: Vec, pool: &Pool, ) -> Result<(), ChaosError> { - is_admin(user_id, organisation_id, pool).await?; - sqlx::query!( "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", organisation_id, @@ -178,8 +173,6 @@ pub async fn remove_admin_from_organisation( admin_to_remove: i64, pool: &Pool, ) -> Result<(), ChaosError> { - is_admin(user_id, organisation_id, pool).await?; - sqlx::query!( " DELETE FROM organisation_members WHERE user_id = $1 AND organisation_id = $2 From a472be23ea4ffb08f491710c0032ffe9659ada85 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 15:35:41 +1000 Subject: [PATCH 27/36] use `Transaction` when doing multiple queries --- backend/server/src/handler/organisation.rs | 47 ++++++++++++---------- backend/server/src/models/mod.rs | 5 ++- backend/server/src/models/organisation.rs | 4 +- backend/server/src/models/transaction.rs | 27 +++++++++++++ backend/server/src/service/organisation.rs | 45 +++++++++++++-------- 5 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 backend/server/src/models/transaction.rs diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 19451c1b..c8e7629e 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,13 +1,14 @@ use crate::models; use crate::models::app::AppState; -use crate::models::auth::{AuthUser, OrganisationAdmin}; use crate::models::auth::SuperUser; +use crate::models::auth::{OrganisationAdmin}; +use crate::models::error::ChaosError; use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation}; +use crate::models::transaction::DBTransaction; use crate::service; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; -use crate::models::error::ChaosError; pub async fn get_organisation( State(state): State, @@ -22,7 +23,9 @@ pub async fn update_organisation_logo( Path(organisation_id): Path, admin: OrganisationAdmin, ) -> Result { - let logo_url = service::organisation::update_organisation_logo(organisation_id, admin.user_id, &state.db).await?; + let logo_url = + service::organisation::update_organisation_logo(organisation_id, admin.user_id, &state.db) + .await?; Ok((StatusCode::OK, Json(logo_url))) } @@ -38,26 +41,19 @@ pub async fn delete_organisation( pub async fn create_organisation( State(state): State, _user: SuperUser, + mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { - let mut transaction = state.db.begin().await?; - - match service::organisation::create_organisation( + service::organisation::create_organisation( data.admin, data.name, state.snowflake_generator, - &mut transaction, + &mut transaction.tx, ) - .await { - Ok(..) => { - transaction.commit().await?; - Ok((StatusCode::OK, "Successfully created organisation")) - }, - Err(err) => { - transaction.rollback().await?; - Err(err) - } - } + .await?; + + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully created organisation")) } pub async fn get_organisation_members( @@ -65,12 +61,14 @@ pub async fn get_organisation_members( Path(organisation_id): Path, admin: OrganisationAdmin, ) -> Result { - let members = service::organisation::get_organisation_members(organisation_id, admin.user_id, &state.db).await?; + let members = + service::organisation::get_organisation_members(organisation_id, admin.user_id, &state.db) + .await?; Ok((StatusCode::OK, Json(members))) } pub async fn update_organisation_admins( - State(state): State, + mut transaction: DBTransaction<'_>, Path(organisation_id): Path, Json(request_body): Json, admin: OrganisationAdmin, @@ -79,10 +77,11 @@ pub async fn update_organisation_admins( organisation_id, admin.user_id, request_body.members, - &state.db, + &mut transaction.tx, ) .await?; + transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully updated organisation admins")) } @@ -100,14 +99,18 @@ pub async fn remove_admin_from_organisation( ) .await?; - Ok((StatusCode::OK, "Successfully removed admin from organisation")) + Ok(( + StatusCode::OK, + "Successfully removed admin from organisation", + )) } pub async fn get_organisation_campaigns( State(state): State, Path(organisation_id): Path, ) -> Result { - let campaigns = service::organisation::get_organisation_campaigns(organisation_id, &state.db).await?; + let campaigns = + service::organisation::get_organisation_campaigns(organisation_id, &state.db).await?; Ok((StatusCode::OK, Json(campaigns))) } diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index c98b769a..b2313e5b 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -1,6 +1,7 @@ pub mod app; pub mod auth; -pub mod error; -pub mod user; pub mod campaign; +pub mod error; pub mod organisation; +pub mod transaction; +pub mod user; diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 5dd7bfad..55de16ef 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -1,7 +1,7 @@ +use crate::models::campaign::Campaign; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; -use crate::models::campaign::Campaign; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Organisation { @@ -55,4 +55,4 @@ pub struct AdminUpdateList { #[derive(Deserialize, Serialize)] pub struct AdminToRemove { pub user_id: i64, -} \ No newline at end of file +} diff --git a/backend/server/src/models/transaction.rs b/backend/server/src/models/transaction.rs new file mode 100644 index 00000000..4cf1ccee --- /dev/null +++ b/backend/server/src/models/transaction.rs @@ -0,0 +1,27 @@ +use crate::models::app::AppState; +use crate::models::error::ChaosError; +use axum::async_trait; +use axum::extract::{FromRef, FromRequestParts}; +use axum::http::request::Parts; +use sqlx::{Postgres, Transaction}; + +pub struct DBTransaction<'a> { + pub tx: Transaction<'a, Postgres>, +} + +#[async_trait] +impl FromRequestParts for DBTransaction<'_> +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(_: &mut Parts, state: &S) -> Result { + let app_state = AppState::from_ref(state); + + Ok(DBTransaction { + tx: app_state.db.begin().await?, + }) + } +} \ No newline at end of file diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index b86deccc..c49c7165 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -1,13 +1,17 @@ -use std::ops::DerefMut; +use crate::models::campaign::Campaign; +use crate::models::error::ChaosError; +use crate::models::organisation::{Member, MemberList, OrganisationDetails, OrganisationRole}; use chrono::{DateTime, Utc}; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres, Transaction}; +use std::ops::DerefMut; use uuid::Uuid; -use crate::models::organisation::{Member, MemberList, OrganisationDetails, OrganisationRole}; -use crate::models::campaign::Campaign; -use crate::models::error::ChaosError; -pub async fn is_admin(user_id: i64, organisation_id: i64, pool: &Pool) -> Result<(), ChaosError> { +pub async fn is_admin( + user_id: i64, + organisation_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { let is_admin = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM organisation_members WHERE organisation_id = $1 AND user_id = $2 AND role = 'Admin')", organisation_id, @@ -17,13 +21,16 @@ pub async fn is_admin(user_id: i64, organisation_id: i64, pool: &Pool) .await?.exists.expect("`exists` should always exist in this query result"); if !is_admin { - return Err(ChaosError::UnauthorizedError) + return Err(ChaosError::UnauthorizedError); } Ok(()) } -pub async fn get_organisation(id: i64, pool: &Pool) -> Result { +pub async fn get_organisation( + id: i64, + pool: &Pool, +) -> Result { Ok(sqlx::query_as!( OrganisationDetails, " @@ -65,7 +72,10 @@ pub async fn update_organisation_logo( Ok(upload_url) } -pub async fn delete_organisation(organisation_id: i64, pool: &Pool) -> Result<(), ChaosError> { +pub async fn delete_organisation( + organisation_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { sqlx::query!( " DELETE FROM organisations WHERE id = $1 @@ -129,25 +139,23 @@ pub async fn get_organisation_members( .fetch_all(pool) .await?; - Ok( - MemberList { - members: admin_list, - } - ) + Ok(MemberList { + members: admin_list, + }) } pub async fn update_organisation_admins( organisation_id: i64, user_id: i64, admin_id_list: Vec, - pool: &Pool, + transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { sqlx::query!( "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", organisation_id, OrganisationRole::Admin as OrganisationRole ) - .execute(pool) + .execute(transaction.deref_mut()) .await?; for admin_id in admin_id_list { @@ -160,7 +168,7 @@ pub async fn update_organisation_admins( admin_id, OrganisationRole::Admin as OrganisationRole ) - .execute(pool) + .execute(transaction.deref_mut()) .await?; } @@ -186,7 +194,10 @@ pub async fn remove_admin_from_organisation( Ok(()) } -pub async fn get_organisation_campaigns(id: i64, pool: &Pool) -> Result, ChaosError> { +pub async fn get_organisation_campaigns( + id: i64, + pool: &Pool, +) -> Result, ChaosError> { Ok(sqlx::query_as!( Campaign, " From d201406d1688208c9eca366c4f85f50643c9d926 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 15:35:57 +1000 Subject: [PATCH 28/36] cargo fmt --- backend/server/src/models/auth.rs | 45 +++++++++++++-------------- backend/server/src/models/campaign.rs | 2 +- backend/server/src/models/error.rs | 20 +++++++++--- backend/server/src/service/auth.rs | 2 +- backend/server/src/service/jwt.rs | 2 +- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 5d839528..d3f9ddcd 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -2,16 +2,13 @@ use crate::models::app::AppState; use crate::models::error::ChaosError; use crate::service::auth::is_super_user; use crate::service::jwt::decode_auth_token; +use crate::service::organisation::is_admin; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; -use axum::{ - async_trait, - RequestPartsExt, -}; +use axum::{async_trait, RequestPartsExt}; use axum_extra::{headers::Cookie, TypedHeader}; use serde::{Deserialize, Serialize}; -use crate::service::organisation::is_admin; #[derive(Deserialize, Serialize)] pub struct AuthRequest { @@ -55,10 +52,12 @@ where .await .map_err(|_| ChaosError::NotLoggedInError)?; - let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedInError)?; + let token = cookies + .get("auth_token") + .ok_or(ChaosError::NotLoggedInError)?; - let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedInError)?; + let claims = decode_auth_token(token, decoding_key, jwt_validator) + .ok_or(ChaosError::NotLoggedInError)?; Ok(AuthUser { user_id: claims.sub, @@ -88,10 +87,12 @@ where .await .map_err(|_| ChaosError::NotLoggedInError)?; - let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedInError)?; + let token = cookies + .get("auth_token") + .ok_or(ChaosError::NotLoggedInError)?; - let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedInError)?; + let claims = decode_auth_token(token, decoding_key, jwt_validator) + .ok_or(ChaosError::NotLoggedInError)?; let pool = &app_state.db; let possible_user = is_super_user(claims.sub, pool).await; @@ -114,9 +115,9 @@ pub struct OrganisationAdmin { #[async_trait] impl FromRequestParts for OrganisationAdmin - where - AppState: FromRef, - S: Send + Sync, +where + AppState: FromRef, + S: Send + Sync, { type Rejection = ChaosError; @@ -129,10 +130,12 @@ impl FromRequestParts for OrganisationAdmin .await .map_err(|_| ChaosError::NotLoggedInError)?; - let token = cookies.get("auth_token").ok_or(ChaosError::NotLoggedInError)?; + let token = cookies + .get("auth_token") + .ok_or(ChaosError::NotLoggedInError)?; - let claims = - decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedInError)?; + let claims = decode_auth_token(token, decoding_key, jwt_validator) + .ok_or(ChaosError::NotLoggedInError)?; let pool = &app_state.db; let user_id = claims.sub; @@ -144,10 +147,6 @@ impl FromRequestParts for OrganisationAdmin is_admin(user_id, organisation_id, pool).await?; - Ok( - OrganisationAdmin { - user_id - } - ) + Ok(OrganisationAdmin { user_id }) } -} \ No newline at end of file +} diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index ce422ec5..a139900c 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -9,4 +9,4 @@ pub struct Campaign { pub cover_image: Option, pub starts_at: DateTime, pub ends_at: DateTime, -} \ No newline at end of file +} diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 533a4f4c..ac2f69e4 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -26,7 +26,13 @@ pub enum ChaosError { ReqwestError(#[from] reqwest::Error), #[error("OAuth2 error")] - OAuthError(#[from] oauth2::RequestTokenError, oauth2::StandardErrorResponse>) + OAuthError( + #[from] + oauth2::RequestTokenError< + oauth2::reqwest::Error, + oauth2::StandardErrorResponse, + >, + ), } /// Implementation for converting errors into responses. Manages error code and message returned. @@ -34,15 +40,19 @@ impl IntoResponse for ChaosError { fn into_response(self) -> Response { match self { ChaosError::NotLoggedInError => Redirect::temporary("/auth/google").into_response(), - ChaosError::UnauthorizedError => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), - ChaosError::ForbiddenOperationError => (StatusCode::FORBIDDEN, "Forbidden operation").into_response(), + ChaosError::UnauthorizedError => { + (StatusCode::UNAUTHORIZED, "Unauthorized").into_response() + } + ChaosError::ForbiddenOperationError => { + (StatusCode::FORBIDDEN, "Forbidden operation").into_response() + } ChaosError::BadRequest => (StatusCode::BAD_REQUEST, "Bad request").into_response(), ChaosError::DatabaseError(db_error) => match db_error { // We only care about the RowNotFound error, as others are miscellaneous DB errors. sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Not found").into_response(), _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response(), }, - _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() + _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response(), } } -} \ No newline at end of file +} diff --git a/backend/server/src/service/auth.rs b/backend/server/src/service/auth.rs index 7da5a35b..cf136cfd 100644 --- a/backend/server/src/service/auth.rs +++ b/backend/server/src/service/auth.rs @@ -1,7 +1,7 @@ +use crate::models::user::UserRole; use anyhow::Result; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; -use crate::models::user::UserRole; /// 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. diff --git a/backend/server/src/service/jwt.rs b/backend/server/src/service/jwt.rs index c7db35cb..bddce804 100644 --- a/backend/server/src/service/jwt.rs +++ b/backend/server/src/service/jwt.rs @@ -1,5 +1,5 @@ -use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation}; use jsonwebtoken::DecodingKey; +use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use uuid::Uuid; From d3e31ac0269ab99b100932c97ef39ae6b36b53f8 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 15:36:41 +1000 Subject: [PATCH 29/36] add org route to app --- backend/server/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 8d91e4b0..639ae23d 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,11 +1,13 @@ +use crate::handler::auth::google_callback; +use crate::handler::organisation::create_organisation; use anyhow::Result; +use axum::routing::post; use axum::{routing::get, Router}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use std::env; -use crate::handler::auth::google_callback; mod handler; mod models; @@ -57,6 +59,7 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) .route("/api/auth/callback/google", get(google_callback)) + .route("/api/v1/organisation", post(create_organisation)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); From ddecb47ad2bfa9793f18df098da3cb2bc7bae017 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 15:52:35 +1000 Subject: [PATCH 30/36] move `Organisation` service functions into `Organisation` struct --- backend/server/src/handler/organisation.rs | 32 +- backend/server/src/models/auth.rs | 4 +- backend/server/src/models/organisation.rs | 258 ++++++++++++- backend/server/src/models/transaction.rs | 2 +- backend/server/src/service/organisation.rs | 417 ++++++++++----------- 5 files changed, 483 insertions(+), 230 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index c8e7629e..4184ff3f 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,9 +1,9 @@ use crate::models; use crate::models::app::AppState; +use crate::models::auth::OrganisationAdmin; use crate::models::auth::SuperUser; -use crate::models::auth::{OrganisationAdmin}; use crate::models::error::ChaosError; -use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation}; +use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation}; use crate::models::transaction::DBTransaction; use crate::service; use axum::extract::{Json, Path, State}; @@ -14,7 +14,7 @@ pub async fn get_organisation( State(state): State, Path(organisation_id): Path, ) -> Result { - let org = service::organisation::get_organisation(organisation_id, &state.db).await?; + let org = Organisation::get(organisation_id, &state.db).await?; Ok((StatusCode::OK, Json(org))) } @@ -24,7 +24,7 @@ pub async fn update_organisation_logo( admin: OrganisationAdmin, ) -> Result { let logo_url = - service::organisation::update_organisation_logo(organisation_id, admin.user_id, &state.db) + Organisation::update_logo(organisation_id, &state.db) .await?; Ok((StatusCode::OK, Json(logo_url))) } @@ -34,7 +34,7 @@ pub async fn delete_organisation( Path(organisation_id): Path, _user: SuperUser, ) -> Result { - service::organisation::delete_organisation(organisation_id, &state.db).await?; + Organisation::delete(organisation_id, &state.db).await?; Ok((StatusCode::OK, "Successfully deleted organisation")) } @@ -44,7 +44,7 @@ pub async fn create_organisation( mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { - service::organisation::create_organisation( + Organisation::create( data.admin, data.name, state.snowflake_generator, @@ -62,38 +62,36 @@ pub async fn get_organisation_members( admin: OrganisationAdmin, ) -> Result { let members = - service::organisation::get_organisation_members(organisation_id, admin.user_id, &state.db) + Organisation::get_members(organisation_id, admin.user_id, &state.db) .await?; Ok((StatusCode::OK, Json(members))) } -pub async fn update_organisation_admins( +pub async fn update_organisation_members( mut transaction: DBTransaction<'_>, Path(organisation_id): Path, Json(request_body): Json, admin: OrganisationAdmin, ) -> Result { - service::organisation::update_organisation_admins( + Organisation::update_members( organisation_id, - admin.user_id, request_body.members, &mut transaction.tx, ) .await?; transaction.tx.commit().await?; - Ok((StatusCode::OK, "Successfully updated organisation admins")) + Ok((StatusCode::OK, "Successfully updated organisation members")) } -pub async fn remove_admin_from_organisation( +pub async fn remove_member_from_organisation( State(state): State, Path(organisation_id): Path, admin: OrganisationAdmin, Json(request_body): Json, ) -> Result { - service::organisation::remove_admin_from_organisation( + Organisation::remove_member( organisation_id, - admin.user_id, request_body.user_id, &state.db, ) @@ -101,7 +99,7 @@ pub async fn remove_admin_from_organisation( Ok(( StatusCode::OK, - "Successfully removed admin from organisation", + "Successfully removed member from organisation", )) } @@ -110,7 +108,7 @@ pub async fn get_organisation_campaigns( Path(organisation_id): Path, ) -> Result { let campaigns = - service::organisation::get_organisation_campaigns(organisation_id, &state.db).await?; + Organisation::get_campaigns(organisation_id, &state.db).await?; Ok((StatusCode::OK, Json(campaigns))) } @@ -123,7 +121,7 @@ pub async fn create_campaign_for_organisation( let mut snowflake_generator = state.snowflake_generator; let new_campaign_id = snowflake_generator.real_time_generate(); - service::organisation::create_campaign_for_organisation( + Organisation::create_campaign( new_campaign_id, request_body.name, request_body.description, diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index d3f9ddcd..05a899b9 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::auth::is_super_user; use crate::service::jwt::decode_auth_token; -use crate::service::organisation::is_admin; +use crate::service::organisation::user_is_admin; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use axum::response::{IntoResponse, Redirect, Response}; @@ -145,7 +145,7 @@ where .await .map_err(|_| ChaosError::BadRequest)?; - is_admin(user_id, organisation_id, pool).await?; + user_is_admin(user_id, organisation_id, pool).await?; Ok(OrganisationAdmin { user_id }) } diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 55de16ef..6d95c827 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -1,7 +1,11 @@ use crate::models::campaign::Campaign; +use crate::models::error::ChaosError; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; +use snowflake::SnowflakeIdGenerator; +use sqlx::{FromRow, Pool, Postgres, Transaction}; +use std::ops::DerefMut; +use uuid::Uuid; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Organisation { @@ -56,3 +60,255 @@ pub struct AdminUpdateList { pub struct AdminToRemove { pub user_id: i64, } + +impl Organisation { + pub async fn create( + admin_id: i64, + name: String, + mut snowflake_generator: SnowflakeIdGenerator, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + let id = snowflake_generator.generate(); + + sqlx::query!( + " + INSERT INTO organisations (id, name) + VALUES ($1, $2) + ", + id, + name + ) + .execute(transaction.deref_mut()) + .await?; + + sqlx::query!( + " + INSERT INTO organisation_members (organisation_id, user_id, role) + VALUES ($1, $2, $3) + ", + id, + admin_id, + OrganisationRole::Admin as OrganisationRole + ) + .execute(transaction.deref_mut()) + .await?; + + Ok(()) + } + + pub async fn get(id: i64, pool: &Pool) -> Result { + let organisation = sqlx::query_as!( + OrganisationDetails, + " + SELECT id, name, logo, created_at + FROM organisations + WHERE id = $1 + ", + id + ) + .fetch_one(pool) + .await?; + + Ok(organisation) + } + + pub async fn delete(id: i64, pool: &Pool) -> Result<(), ChaosError> { + sqlx::query!( + " + DELETE FROM organisations WHERE id = $1 + ", + id + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn get_members( + organisation_id: i64, + user_id: i64, + pool: &Pool, + ) -> Result { + let admin_list = sqlx::query_as!( + Member, + " + SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members + LEFT JOIN users on users.id = organisation_members.user_id + WHERE organisation_id = $1 + ", + organisation_id + ) + .fetch_all(pool) + .await?; + + Ok(MemberList { + members: admin_list, + }) + } + + pub async fn update_admins( + organisation_id: i64, + admin_id_list: Vec, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + sqlx::query!( + "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", + organisation_id, + OrganisationRole::Admin as OrganisationRole + ) + .execute(transaction.deref_mut()) + .await?; + + for admin_id in admin_id_list { + sqlx::query!( + " + INSERT INTO organisation_members (organisation_id, user_id, role) + VALUES ($1, $2, $3) + ", + organisation_id, + admin_id, + OrganisationRole::Admin as OrganisationRole + ) + .execute(transaction.deref_mut()) + .await?; + } + + Ok(()) + } + + pub async fn update_members( + organisation_id: i64, + member_id_list: Vec, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + sqlx::query!( + "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", + organisation_id, + OrganisationRole::User as OrganisationRole + ) + .execute(transaction.deref_mut()) + .await?; + + for member_id in member_id_list { + sqlx::query!( + " + INSERT INTO organisation_members (organisation_id, user_id, role) + VALUES ($1, $2, $3) + ", + organisation_id, + member_id, + OrganisationRole::User as OrganisationRole + ) + .execute(transaction.deref_mut()) + .await?; + } + + Ok(()) + } + + pub async fn remove_admin( + organisation_id: i64, + admin_to_remove: i64, + pool: &Pool, + ) -> Result<(), ChaosError> { + sqlx::query!( + " + UPDATE organisation_members SET role = $3 WHERE user_id = $1 AND organisation_id = $2 + ", + admin_to_remove, + organisation_id, + OrganisationRole::User as OrganisationRole + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn remove_member( + organisation_id: i64, + user_id: i64, + pool: &Pool, + ) -> Result<(), ChaosError> { + sqlx::query!( + " + DELETE FROM organisation_members WHERE user_id = $1 AND organisation_id = $2 + ", + user_id, + organisation_id + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn update_logo(id: i64, pool: &Pool) -> Result { + let dt = Utc::now(); + + let logo_id = Uuid::new_v4().to_string(); // TODO: Change db type to UUID + let current_time = dt; + sqlx::query!( + " + UPDATE organisations + SET logo = $2, updated_at = $3 + WHERE id = $1 + ", + id, + logo_id, + current_time + ) + .execute(pool) + .await?; + + // TODO: Generate a s3 url + let upload_url = "GENERATE AN S3 PRESIGNED URL".to_string(); + + Ok(upload_url) + } + + pub async fn get_campaigns( + organisation_id: i64, + pool: &Pool, + ) -> Result, ChaosError> { + let campaigns = sqlx::query_as!( + Campaign, + " + SELECT id, name, cover_image, description, starts_at, ends_at + FROM campaigns + WHERE organisation_id = $1 + ", + organisation_id + ) + .fetch_all(pool) + .await?; + + Ok(campaigns) + } + + pub async fn create_campaign( + id: i64, + name: String, + description: Option, + starts_at: DateTime, + ends_at: DateTime, + pool: &Pool, + ) -> Result<(), ChaosError> { + sqlx::query!( + " + INSERT INTO campaigns (id, name, description, starts_at, ends_at) + VALUES ($1, $2, $3, $4, $5) + ", + id, + name, + description, + starts_at, + ends_at + ) + .execute(pool) + .await?; + + Ok(()) + } +} diff --git a/backend/server/src/models/transaction.rs b/backend/server/src/models/transaction.rs index 4cf1ccee..529f6320 100644 --- a/backend/server/src/models/transaction.rs +++ b/backend/server/src/models/transaction.rs @@ -24,4 +24,4 @@ where tx: app_state.db.begin().await?, }) } -} \ No newline at end of file +} diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index c49c7165..273444bc 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -7,7 +7,7 @@ use sqlx::{Pool, Postgres, Transaction}; use std::ops::DerefMut; use uuid::Uuid; -pub async fn is_admin( +pub async fn user_is_admin( user_id: i64, organisation_id: i64, pool: &Pool, @@ -27,211 +27,210 @@ pub async fn is_admin( Ok(()) } -pub async fn get_organisation( - id: i64, - pool: &Pool, -) -> Result { - Ok(sqlx::query_as!( - OrganisationDetails, - " - SELECT id, name, logo, created_at - FROM organisations - WHERE id = $1 - ", - id - ) - .fetch_one(pool) - .await?) -} - -pub async fn update_organisation_logo( - id: i64, - user_id: i64, - pool: &Pool, -) -> Result { - let dt = Utc::now(); - - let logo_id = Uuid::new_v4().to_string(); // TODO: Change db type to UUID - let current_time = dt; - sqlx::query!( - " - UPDATE organisations - SET logo = $2, updated_at = $3 - WHERE id = $1 - ", - id, - logo_id, - current_time - ) - .execute(pool) - .await?; - - // TODO: Generate a s3 url - let upload_url = "GENERATE AN S3 PRESIGNED URL".to_string(); - - Ok(upload_url) -} - -pub async fn delete_organisation( - organisation_id: i64, - pool: &Pool, -) -> Result<(), ChaosError> { - sqlx::query!( - " - DELETE FROM organisations WHERE id = $1 - ", - organisation_id - ) - .execute(pool) - .await?; - - Ok(()) -} - -pub async fn create_organisation( - admin_id: i64, - name: String, - mut snowflake_generator: SnowflakeIdGenerator, - transaction: &mut Transaction<'_, Postgres>, -) -> Result<(), ChaosError> { - let id = snowflake_generator.generate(); - - sqlx::query!( - " - INSERT INTO organisations (id, name) - VALUES ($1, $2) - ", - id, - name - ) - .execute(transaction.deref_mut()) - .await?; - - sqlx::query!( - " - INSERT INTO organisation_members (organisation_id, user_id, role) - VALUES ($1, $2, $3) - ", - id, - admin_id, - OrganisationRole::Admin as OrganisationRole - ) - .execute(transaction.deref_mut()) - .await?; - - Ok(()) -} - -pub async fn get_organisation_members( - organisation_id: i64, - user_id: i64, - pool: &Pool, -) -> Result { - let admin_list = sqlx::query_as!( - Member, - " - SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members - LEFT JOIN users on users.id = organisation_members.user_id - WHERE organisation_id = $1 - ", - organisation_id - ) - .fetch_all(pool) - .await?; - - Ok(MemberList { - members: admin_list, - }) -} - -pub async fn update_organisation_admins( - organisation_id: i64, - user_id: i64, - admin_id_list: Vec, - transaction: &mut Transaction<'_, Postgres>, -) -> Result<(), ChaosError> { - sqlx::query!( - "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", - organisation_id, - OrganisationRole::Admin as OrganisationRole - ) - .execute(transaction.deref_mut()) - .await?; - - for admin_id in admin_id_list { - sqlx::query!( - " - INSERT INTO organisation_members (organisation_id, user_id, role) - VALUES ($1, $2, $3) - ", - organisation_id, - admin_id, - OrganisationRole::Admin as OrganisationRole - ) - .execute(transaction.deref_mut()) - .await?; - } - - Ok(()) -} - -pub async fn remove_admin_from_organisation( - organisation_id: i64, - user_id: i64, - admin_to_remove: i64, - pool: &Pool, -) -> Result<(), ChaosError> { - sqlx::query!( - " - DELETE FROM organisation_members WHERE user_id = $1 AND organisation_id = $2 - ", - admin_to_remove, - organisation_id - ) - .execute(pool) - .await?; - - Ok(()) -} - -pub async fn get_organisation_campaigns( - id: i64, - pool: &Pool, -) -> Result, ChaosError> { - Ok(sqlx::query_as!( - Campaign, - " - SELECT id, name, cover_image, description, starts_at, ends_at - FROM campaigns - WHERE organisation_id = $1 - ", - id - ) - .fetch_all(pool) - .await?) -} - -pub async fn create_campaign_for_organisation( - id: i64, - name: String, - description: Option, - starts_at: DateTime, - ends_at: DateTime, - pool: &Pool, -) -> Result<(), ChaosError> { - sqlx::query!( - " - INSERT INTO campaigns (id, name, description, starts_at, ends_at) - VALUES ($1, $2, $3, $4, $5) - ", - id, - name, - description, - starts_at, - ends_at - ) - .execute(pool) - .await?; - - Ok(()) -} +// pub async fn get_organisation( +// id: i64, +// pool: &Pool, +// ) -> Result { +// Ok(sqlx::query_as!( +// OrganisationDetails, +// " +// SELECT id, name, logo, created_at +// FROM organisations +// WHERE id = $1 +// ", +// id +// ) +// .fetch_one(pool) +// .await?) +// } + +// pub async fn update_organisation_logo( +// id: i64, +// user_id: i64, +// pool: &Pool, +// ) -> Result { +// let dt = Utc::now(); +// +// let logo_id = Uuid::new_v4().to_string(); // TODO: Change db type to UUID +// let current_time = dt; +// sqlx::query!( +// " +// UPDATE organisations +// SET logo = $2, updated_at = $3 +// WHERE id = $1 +// ", +// id, +// logo_id, +// current_time +// ) +// .execute(pool) +// .await?; +// +// let upload_url = "GENERATE AN S3 PRESIGNED URL".to_string(); +// +// Ok(upload_url) +// } + +// pub async fn delete_organisation( +// organisation_id: i64, +// pool: &Pool, +// ) -> Result<(), ChaosError> { +// sqlx::query!( +// " +// DELETE FROM organisations WHERE id = $1 +// ", +// organisation_id +// ) +// .execute(pool) +// .await?; +// +// Ok(()) +// } + +// pub async fn create_organisation( +// admin_id: i64, +// name: String, +// mut snowflake_generator: SnowflakeIdGenerator, +// transaction: &mut Transaction<'_, Postgres>, +// ) -> Result<(), ChaosError> { +// let id = snowflake_generator.generate(); +// +// sqlx::query!( +// " +// INSERT INTO organisations (id, name) +// VALUES ($1, $2) +// ", +// id, +// name +// ) +// .execute(transaction.deref_mut()) +// .await?; +// +// sqlx::query!( +// " +// INSERT INTO organisation_members (organisation_id, user_id, role) +// VALUES ($1, $2, $3) +// ", +// id, +// admin_id, +// OrganisationRole::Admin as OrganisationRole +// ) +// .execute(transaction.deref_mut()) +// .await?; +// +// Ok(()) +// } + +// pub async fn get_organisation_members( +// organisation_id: i64, +// user_id: i64, +// pool: &Pool, +// ) -> Result { +// let admin_list = sqlx::query_as!( +// Member, +// " +// SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members +// LEFT JOIN users on users.id = organisation_members.user_id +// WHERE organisation_id = $1 +// ", +// organisation_id +// ) +// .fetch_all(pool) +// .await?; +// +// Ok(MemberList { +// members: admin_list, +// }) +// } + +// pub async fn update_organisation_admins( +// organisation_id: i64, +// user_id: i64, +// admin_id_list: Vec, +// transaction: &mut Transaction<'_, Postgres>, +// ) -> Result<(), ChaosError> { +// sqlx::query!( +// "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", +// organisation_id, +// OrganisationRole::Admin as OrganisationRole +// ) +// .execute(transaction.deref_mut()) +// .await?; +// +// for admin_id in admin_id_list { +// sqlx::query!( +// " +// INSERT INTO organisation_members (organisation_id, user_id, role) +// VALUES ($1, $2, $3) +// ", +// organisation_id, +// admin_id, +// OrganisationRole::Admin as OrganisationRole +// ) +// .execute(transaction.deref_mut()) +// .await?; +// } +// +// Ok(()) +// } + +// pub async fn remove_admin_from_organisation( +// organisation_id: i64, +// user_id: i64, +// admin_to_remove: i64, +// pool: &Pool, +// ) -> Result<(), ChaosError> { +// sqlx::query!( +// " +// DELETE FROM organisation_members WHERE user_id = $1 AND organisation_id = $2 +// ", +// admin_to_remove, +// organisation_id +// ) +// .execute(pool) +// .await?; +// +// Ok(()) +// } + +// pub async fn get_organisation_campaigns( +// id: i64, +// pool: &Pool, +// ) -> Result, ChaosError> { +// Ok(sqlx::query_as!( +// Campaign, +// " +// SELECT id, name, cover_image, description, starts_at, ends_at +// FROM campaigns +// WHERE organisation_id = $1 +// ", +// id +// ) +// .fetch_all(pool) +// .await?) +// } + +// pub async fn create_campaign_for_organisation( +// id: i64, +// name: String, +// description: Option, +// starts_at: DateTime, +// ends_at: DateTime, +// pool: &Pool, +// ) -> Result<(), ChaosError> { +// sqlx::query!( +// " +// INSERT INTO campaigns (id, name, description, starts_at, ends_at) +// VALUES ($1, $2, $3, $4, $5) +// ", +// id, +// name, +// description, +// starts_at, +// ends_at +// ) +// .execute(pool) +// .await?; +// +// Ok(()) +// } From a00ce1eaae8687b39bf64d2d65b80b7b4d314ae4 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 16:12:45 +1000 Subject: [PATCH 31/36] moved org handlers into `OrganisationHandler` struct --- backend/server/src/handler/organisation.rs | 263 ++++++++++++--------- backend/server/src/main.rs | 4 +- backend/server/src/models/organisation.rs | 7 +- backend/server/src/service/organisation.rs | 208 ---------------- 4 files changed, 157 insertions(+), 325 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 4184ff3f..5c352eeb 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,6 +1,6 @@ use crate::models; use crate::models::app::AppState; -use crate::models::auth::OrganisationAdmin; +use crate::models::auth::{AuthUser, OrganisationAdmin}; use crate::models::auth::SuperUser; use crate::models::error::ChaosError; use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation}; @@ -10,126 +10,165 @@ use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; -pub async fn get_organisation( - State(state): State, - Path(organisation_id): Path, -) -> Result { - let org = Organisation::get(organisation_id, &state.db).await?; - Ok((StatusCode::OK, Json(org))) -} +pub struct OrganisationHandler; -pub async fn update_organisation_logo( - State(state): State, - Path(organisation_id): Path, - admin: OrganisationAdmin, -) -> Result { - let logo_url = - Organisation::update_logo(organisation_id, &state.db) +impl OrganisationHandler { + pub async fn create( + State(state): State, + _user: SuperUser, + mut transaction: DBTransaction<'_>, + Json(data): Json, + ) -> Result { + Organisation::create( + data.admin, + data.name, + state.snowflake_generator, + &mut transaction.tx, + ) .await?; - Ok((StatusCode::OK, Json(logo_url))) -} -pub async fn delete_organisation( - State(state): State, - Path(organisation_id): Path, - _user: SuperUser, -) -> Result { - Organisation::delete(organisation_id, &state.db).await?; - Ok((StatusCode::OK, "Successfully deleted organisation")) -} + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully created organisation")) + } -pub async fn create_organisation( - State(state): State, - _user: SuperUser, - mut transaction: DBTransaction<'_>, - Json(data): Json, -) -> Result { - Organisation::create( - data.admin, - data.name, - state.snowflake_generator, - &mut transaction.tx, - ) - .await?; - - transaction.tx.commit().await?; - Ok((StatusCode::OK, "Successfully created organisation")) -} + pub async fn get( + State(state): State, + Path(organisation_id): Path, + _user: AuthUser, + ) -> Result { + let org = Organisation::get(organisation_id, &state.db).await?; + Ok((StatusCode::OK, Json(org))) + } + + pub async fn delete( + State(state): State, + Path(organisation_id): Path, + _user: SuperUser, + ) -> Result { + Organisation::delete(organisation_id, &state.db).await?; + Ok((StatusCode::OK, "Successfully deleted organisation")) + } + + pub async fn get_members( + State(state): State, + Path(organisation_id): Path, + _admin: OrganisationAdmin, + ) -> Result { + let members = + Organisation::get_members(organisation_id, &state.db) + .await?; + Ok((StatusCode::OK, Json(members))) + } -pub async fn get_organisation_members( - State(state): State, - Path(organisation_id): Path, - admin: OrganisationAdmin, -) -> Result { - let members = - Organisation::get_members(organisation_id, admin.user_id, &state.db) + pub async fn update_admins( + mut transaction: DBTransaction<'_>, + Path(organisation_id): Path, + Json(request_body): Json, + _super_user: SuperUser, + ) -> Result { + Organisation::update_admins( + organisation_id, + request_body.members, + &mut transaction.tx, + ) .await?; - Ok((StatusCode::OK, Json(members))) -} -pub async fn update_organisation_members( - mut transaction: DBTransaction<'_>, - Path(organisation_id): Path, - Json(request_body): Json, - admin: OrganisationAdmin, -) -> Result { - Organisation::update_members( - organisation_id, - request_body.members, - &mut transaction.tx, - ) - .await?; - - transaction.tx.commit().await?; - Ok((StatusCode::OK, "Successfully updated organisation members")) -} + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully updated organisation members")) + } -pub async fn remove_member_from_organisation( - State(state): State, - Path(organisation_id): Path, - admin: OrganisationAdmin, - Json(request_body): Json, -) -> Result { - Organisation::remove_member( - organisation_id, - request_body.user_id, - &state.db, - ) - .await?; - - Ok(( - StatusCode::OK, - "Successfully removed member from organisation", - )) -} + pub async fn update_members( + mut transaction: DBTransaction<'_>, + Path(organisation_id): Path, + Json(request_body): Json, + _admin: OrganisationAdmin, + ) -> Result { + Organisation::update_members( + organisation_id, + request_body.members, + &mut transaction.tx, + ) + .await?; -pub async fn get_organisation_campaigns( - State(state): State, - Path(organisation_id): Path, -) -> Result { - let campaigns = - Organisation::get_campaigns(organisation_id, &state.db).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully updated organisation members")) + } - Ok((StatusCode::OK, Json(campaigns))) -} + pub async fn remove_admin( + State(state): State, + Path(organisation_id): Path, + _super_user: SuperUser, + Json(request_body): Json, + ) -> Result { + Organisation::remove_admin( + organisation_id, + request_body.user_id, + &state.db, + ) + .await?; + + Ok(( + StatusCode::OK, + "Successfully removed member from organisation", + )) + } + + pub async fn remove_member( + State(state): State, + Path(organisation_id): Path, + _admin: OrganisationAdmin, + Json(request_body): Json, + ) -> Result { + Organisation::remove_member( + organisation_id, + request_body.user_id, + &state.db, + ) + .await?; + + Ok(( + StatusCode::OK, + "Successfully removed member from organisation", + )) + } + + pub async fn update_logo( + State(state): State, + Path(organisation_id): Path, + _admin: OrganisationAdmin, + ) -> Result { + let logo_url = + Organisation::update_logo(organisation_id, &state.db) + .await?; + Ok((StatusCode::OK, Json(logo_url))) + } + + pub async fn get_campaigns( + State(state): State, + Path(organisation_id): Path, + _user: AuthUser, + ) -> Result { + let campaigns = + Organisation::get_campaigns(organisation_id, &state.db).await?; + + Ok((StatusCode::OK, Json(campaigns))) + } + + pub async fn create_campaign( + State(mut state): State, + _admin: OrganisationAdmin, + Json(request_body): Json, + ) -> Result { + Organisation::create_campaign( + request_body.name, + request_body.description, + request_body.starts_at, + request_body.ends_at, + &state.db, + &mut state.snowflake_generator + ) + .await?; -pub async fn create_campaign_for_organisation( - State(state): State, - _user: SuperUser, - Json(request_body): Json, -) -> Result { - let mut snowflake_generator = state.snowflake_generator; - let new_campaign_id = snowflake_generator.real_time_generate(); - - Organisation::create_campaign( - new_campaign_id, - request_body.name, - request_body.description, - request_body.starts_at, - request_body.ends_at, - &state.db, - ) - .await?; - - Ok((StatusCode::OK, "Successfully created campaign")) + Ok((StatusCode::OK, "Successfully created campaign")) + } } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 639ae23d..1b0ac8bd 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,5 +1,5 @@ use crate::handler::auth::google_callback; -use crate::handler::organisation::create_organisation; +use crate::handler::organisation::OrganisationHandler; use anyhow::Result; use axum::routing::post; use axum::{routing::get, Router}; @@ -59,7 +59,7 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) .route("/api/auth/callback/google", get(google_callback)) - .route("/api/v1/organisation", post(create_organisation)) + .route("/api/v1/organisation", post(OrganisationHandler::create)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 6d95c827..0f2ed8da 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -127,7 +127,6 @@ impl Organisation { pub async fn get_members( organisation_id: i64, - user_id: i64, pool: &Pool, ) -> Result { let admin_list = sqlx::query_as!( @@ -288,19 +287,21 @@ impl Organisation { } pub async fn create_campaign( - id: i64, name: String, description: Option, starts_at: DateTime, ends_at: DateTime, pool: &Pool, + snowflake_id_generator: &mut SnowflakeIdGenerator ) -> Result<(), ChaosError> { + let new_campaign_id = snowflake_id_generator.real_time_generate(); + sqlx::query!( " INSERT INTO campaigns (id, name, description, starts_at, ends_at) VALUES ($1, $2, $3, $4, $5) ", - id, + new_campaign_id, name, description, starts_at, diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index 273444bc..d3865ca8 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -26,211 +26,3 @@ pub async fn user_is_admin( Ok(()) } - -// pub async fn get_organisation( -// id: i64, -// pool: &Pool, -// ) -> Result { -// Ok(sqlx::query_as!( -// OrganisationDetails, -// " -// SELECT id, name, logo, created_at -// FROM organisations -// WHERE id = $1 -// ", -// id -// ) -// .fetch_one(pool) -// .await?) -// } - -// pub async fn update_organisation_logo( -// id: i64, -// user_id: i64, -// pool: &Pool, -// ) -> Result { -// let dt = Utc::now(); -// -// let logo_id = Uuid::new_v4().to_string(); // TODO: Change db type to UUID -// let current_time = dt; -// sqlx::query!( -// " -// UPDATE organisations -// SET logo = $2, updated_at = $3 -// WHERE id = $1 -// ", -// id, -// logo_id, -// current_time -// ) -// .execute(pool) -// .await?; -// -// let upload_url = "GENERATE AN S3 PRESIGNED URL".to_string(); -// -// Ok(upload_url) -// } - -// pub async fn delete_organisation( -// organisation_id: i64, -// pool: &Pool, -// ) -> Result<(), ChaosError> { -// sqlx::query!( -// " -// DELETE FROM organisations WHERE id = $1 -// ", -// organisation_id -// ) -// .execute(pool) -// .await?; -// -// Ok(()) -// } - -// pub async fn create_organisation( -// admin_id: i64, -// name: String, -// mut snowflake_generator: SnowflakeIdGenerator, -// transaction: &mut Transaction<'_, Postgres>, -// ) -> Result<(), ChaosError> { -// let id = snowflake_generator.generate(); -// -// sqlx::query!( -// " -// INSERT INTO organisations (id, name) -// VALUES ($1, $2) -// ", -// id, -// name -// ) -// .execute(transaction.deref_mut()) -// .await?; -// -// sqlx::query!( -// " -// INSERT INTO organisation_members (organisation_id, user_id, role) -// VALUES ($1, $2, $3) -// ", -// id, -// admin_id, -// OrganisationRole::Admin as OrganisationRole -// ) -// .execute(transaction.deref_mut()) -// .await?; -// -// Ok(()) -// } - -// pub async fn get_organisation_members( -// organisation_id: i64, -// user_id: i64, -// pool: &Pool, -// ) -> Result { -// let admin_list = sqlx::query_as!( -// Member, -// " -// SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members -// LEFT JOIN users on users.id = organisation_members.user_id -// WHERE organisation_id = $1 -// ", -// organisation_id -// ) -// .fetch_all(pool) -// .await?; -// -// Ok(MemberList { -// members: admin_list, -// }) -// } - -// pub async fn update_organisation_admins( -// organisation_id: i64, -// user_id: i64, -// admin_id_list: Vec, -// transaction: &mut Transaction<'_, Postgres>, -// ) -> Result<(), ChaosError> { -// sqlx::query!( -// "DELETE FROM organisation_members WHERE organisation_id = $1 AND role = $2", -// organisation_id, -// OrganisationRole::Admin as OrganisationRole -// ) -// .execute(transaction.deref_mut()) -// .await?; -// -// for admin_id in admin_id_list { -// sqlx::query!( -// " -// INSERT INTO organisation_members (organisation_id, user_id, role) -// VALUES ($1, $2, $3) -// ", -// organisation_id, -// admin_id, -// OrganisationRole::Admin as OrganisationRole -// ) -// .execute(transaction.deref_mut()) -// .await?; -// } -// -// Ok(()) -// } - -// pub async fn remove_admin_from_organisation( -// organisation_id: i64, -// user_id: i64, -// admin_to_remove: i64, -// pool: &Pool, -// ) -> Result<(), ChaosError> { -// sqlx::query!( -// " -// DELETE FROM organisation_members WHERE user_id = $1 AND organisation_id = $2 -// ", -// admin_to_remove, -// organisation_id -// ) -// .execute(pool) -// .await?; -// -// Ok(()) -// } - -// pub async fn get_organisation_campaigns( -// id: i64, -// pool: &Pool, -// ) -> Result, ChaosError> { -// Ok(sqlx::query_as!( -// Campaign, -// " -// SELECT id, name, cover_image, description, starts_at, ends_at -// FROM campaigns -// WHERE organisation_id = $1 -// ", -// id -// ) -// .fetch_all(pool) -// .await?) -// } - -// pub async fn create_campaign_for_organisation( -// id: i64, -// name: String, -// description: Option, -// starts_at: DateTime, -// ends_at: DateTime, -// pool: &Pool, -// ) -> Result<(), ChaosError> { -// sqlx::query!( -// " -// INSERT INTO campaigns (id, name, description, starts_at, ends_at) -// VALUES ($1, $2, $3, $4, $5) -// ", -// id, -// name, -// description, -// starts_at, -// ends_at -// ) -// .execute(pool) -// .await?; -// -// Ok(()) -// } From f67321349b4a7a1c3ce8dcb7bf00056bc796def9 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 16:12:53 +1000 Subject: [PATCH 32/36] ran cargo fmt --- backend/server/src/handler/organisation.rs | 45 ++++++---------------- backend/server/src/models/organisation.rs | 2 +- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 5c352eeb..4eb0fc73 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,7 +1,7 @@ use crate::models; use crate::models::app::AppState; -use crate::models::auth::{AuthUser, OrganisationAdmin}; 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::transaction::DBTransaction; @@ -25,7 +25,7 @@ impl OrganisationHandler { state.snowflake_generator, &mut transaction.tx, ) - .await?; + .await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully created organisation")) @@ -54,9 +54,7 @@ impl OrganisationHandler { Path(organisation_id): Path, _admin: OrganisationAdmin, ) -> Result { - let members = - Organisation::get_members(organisation_id, &state.db) - .await?; + let members = Organisation::get_members(organisation_id, &state.db).await?; Ok((StatusCode::OK, Json(members))) } @@ -66,11 +64,7 @@ impl OrganisationHandler { Json(request_body): Json, _super_user: SuperUser, ) -> Result { - Organisation::update_admins( - organisation_id, - request_body.members, - &mut transaction.tx, - ) + Organisation::update_admins(organisation_id, request_body.members, &mut transaction.tx) .await?; transaction.tx.commit().await?; @@ -83,11 +77,7 @@ impl OrganisationHandler { Json(request_body): Json, _admin: OrganisationAdmin, ) -> Result { - Organisation::update_members( - organisation_id, - request_body.members, - &mut transaction.tx, - ) + Organisation::update_members(organisation_id, request_body.members, &mut transaction.tx) .await?; transaction.tx.commit().await?; @@ -100,12 +90,7 @@ impl OrganisationHandler { _super_user: SuperUser, Json(request_body): Json, ) -> Result { - Organisation::remove_admin( - organisation_id, - request_body.user_id, - &state.db, - ) - .await?; + Organisation::remove_admin(organisation_id, request_body.user_id, &state.db).await?; Ok(( StatusCode::OK, @@ -119,12 +104,7 @@ impl OrganisationHandler { _admin: OrganisationAdmin, Json(request_body): Json, ) -> Result { - Organisation::remove_member( - organisation_id, - request_body.user_id, - &state.db, - ) - .await?; + Organisation::remove_member(organisation_id, request_body.user_id, &state.db).await?; Ok(( StatusCode::OK, @@ -137,9 +117,7 @@ impl OrganisationHandler { Path(organisation_id): Path, _admin: OrganisationAdmin, ) -> Result { - let logo_url = - Organisation::update_logo(organisation_id, &state.db) - .await?; + let logo_url = Organisation::update_logo(organisation_id, &state.db).await?; Ok((StatusCode::OK, Json(logo_url))) } @@ -148,8 +126,7 @@ impl OrganisationHandler { Path(organisation_id): Path, _user: AuthUser, ) -> Result { - let campaigns = - Organisation::get_campaigns(organisation_id, &state.db).await?; + let campaigns = Organisation::get_campaigns(organisation_id, &state.db).await?; Ok((StatusCode::OK, Json(campaigns))) } @@ -165,9 +142,9 @@ impl OrganisationHandler { request_body.starts_at, request_body.ends_at, &state.db, - &mut state.snowflake_generator + &mut state.snowflake_generator, ) - .await?; + .await?; Ok((StatusCode::OK, "Successfully created campaign")) } diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 0f2ed8da..d5dd221d 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -292,7 +292,7 @@ impl Organisation { starts_at: DateTime, ends_at: DateTime, pool: &Pool, - snowflake_id_generator: &mut SnowflakeIdGenerator + snowflake_id_generator: &mut SnowflakeIdGenerator, ) -> Result<(), ChaosError> { let new_campaign_id = snowflake_id_generator.real_time_generate(); From 90702f612133734392881b8ab80b3301cf009e45 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 16:29:10 +1000 Subject: [PATCH 33/36] add S3 url generation to logo update --- backend/server/Cargo.toml | 1 + backend/server/src/handler/organisation.rs | 3 +- backend/server/src/main.rs | 5 ++ backend/server/src/models/app.rs | 2 + backend/server/src/models/error.rs | 3 ++ backend/server/src/models/mod.rs | 1 + backend/server/src/models/organisation.rs | 14 ++++-- backend/server/src/models/storage.rs | 54 ++++++++++++++++++++++ 8 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 backend/server/src/models/storage.rs diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 6f556cdd..9c4babe2 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -22,6 +22,7 @@ chrono = { version = "0.4", features = ["serde"] } oauth2 = "4.4" log = "0.4" uuid = { version = "1.5", features = ["serde", "v4"] } +rust-s3 = "0.34.0" rs-snowflake = "0.6" jsonwebtoken = "9.1" dotenvy = "0.15" diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 4eb0fc73..a6c189b1 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -117,7 +117,8 @@ impl OrganisationHandler { Path(organisation_id): Path, _admin: OrganisationAdmin, ) -> Result { - let logo_url = Organisation::update_logo(organisation_id, &state.db).await?; + let logo_url = + Organisation::update_logo(organisation_id, &state.db, &state.storage_bucket).await?; Ok((StatusCode::OK, Json(logo_url))) } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 1b0ac8bd..52d544df 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,5 +1,6 @@ use crate::handler::auth::google_callback; use crate::handler::organisation::OrganisationHandler; +use crate::models::storage::Storage; use anyhow::Result; use axum::routing::post; use axum::{routing::get, Router}; @@ -45,6 +46,9 @@ async fn main() -> Result<()> { // Initialise Snowflake Generator let snowflake_generator = SnowflakeIdGenerator::new(1, 1); + // Initialise S3 bucket + let storage_bucket = Storage::init_bucket(); + // Add all data to AppState let state = AppState { db: pool, @@ -54,6 +58,7 @@ async fn main() -> Result<()> { jwt_header, jwt_validator, snowflake_generator, + storage_bucket, }; let app = Router::new() diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index 9fbd2a8e..19ecfbff 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -1,5 +1,6 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use reqwest::Client as ReqwestClient; +use s3::Bucket; use snowflake::SnowflakeIdGenerator; use sqlx::{Pool, Postgres}; @@ -12,4 +13,5 @@ pub struct AppState { pub jwt_header: Header, pub jwt_validator: Validation, pub snowflake_generator: SnowflakeIdGenerator, + pub storage_bucket: Bucket, } diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index ac2f69e4..b86d7a4a 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -33,6 +33,9 @@ pub enum ChaosError { oauth2::StandardErrorResponse, >, ), + + #[error("S3 error")] + StorageError(#[from] s3::error::S3Error), } /// 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 b2313e5b..e5a5edb3 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -3,5 +3,6 @@ pub mod auth; pub mod campaign; pub mod error; pub mod organisation; +pub mod storage; pub mod transaction; pub mod user; diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index d5dd221d..8c7f201e 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -1,6 +1,8 @@ use crate::models::campaign::Campaign; use crate::models::error::ChaosError; +use crate::models::storage::Storage; use chrono::{DateTime, Utc}; +use s3::Bucket; use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{FromRow, Pool, Postgres, Transaction}; @@ -243,7 +245,11 @@ impl Organisation { Ok(()) } - pub async fn update_logo(id: i64, pool: &Pool) -> Result { + pub async fn update_logo( + id: i64, + pool: &Pool, + storage_bucket: &Bucket, + ) -> Result { let dt = Utc::now(); let logo_id = Uuid::new_v4().to_string(); // TODO: Change db type to UUID @@ -261,8 +267,10 @@ impl Organisation { .execute(pool) .await?; - // TODO: Generate a s3 url - let upload_url = "GENERATE AN S3 PRESIGNED URL".to_string(); + // TODO: Handle MIME type on FE and BE and handle in S3 upload + let image_id = Uuid::new_v4(); + let upload_url = + Storage::generate_put_url(format!("/{id}/{image_id}"), storage_bucket).await?; Ok(upload_url) } diff --git a/backend/server/src/models/storage.rs b/backend/server/src/models/storage.rs new file mode 100644 index 00000000..53b63b96 --- /dev/null +++ b/backend/server/src/models/storage.rs @@ -0,0 +1,54 @@ +use crate::models::error::ChaosError; +use s3::creds::Credentials; +use s3::{Bucket, BucketConfiguration, Region}; +use std::env; + +pub struct Storage; + +impl Storage { + pub fn init_bucket() -> Bucket { + let bucket_name = env::var("S3_BUCKET_NAME") + .expect("Error getting S3 BUCKET NAME") + .to_string(); + let access_key = env::var("S3_ACCESS_KEY") + .expect("Error getting S3 CREDENTIALS") + .to_string(); + let secret_key = env::var("S3_SECRET_KEY") + .expect("Error getting S3 CREDENTIALS") + .to_string(); + let endpoint = env::var("S3_ENDPOINT") + .expect("Error getting S3 ENDPOINT") + .to_string(); + let region_name = env::var("S3_REGION_NAME") + .expect("Error getting S3 REGION NAME") + .to_string(); + + let credentials = Credentials::new( + Option::from(access_key.as_str()), + Option::from(secret_key.as_str()), + None, + None, + None, + ) + .unwrap(); + + let region = Region::Custom { + region: region_name, + endpoint, + }; + + let config = BucketConfiguration::default(); + + let mut bucket = Bucket::new(&*bucket_name, region, credentials).unwrap(); + // TODO: Change depending on style used by provider + // bucket.set_path_style(); + + bucket + } + + pub async fn generate_put_url(path: String, bucket: &Bucket) -> Result { + let url = bucket.presign_put(path, 3600, None).await?; + + Ok(url) + } +} From 0b9f02827900ee61018942f073d633e6a3059eeb Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 16:37:59 +1000 Subject: [PATCH 34/36] fixed error renaming --- backend/server/src/models/auth.rs | 16 ++++++++-------- backend/server/src/service/organisation.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 05058bb3..17d18e31 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -50,14 +50,14 @@ where let TypedHeader(cookies) = parts .extract::>() .await - .map_err(|_| ChaosError::NotLoggedInError)?; + .map_err(|_| ChaosError::NotLoggedIn)?; let token = cookies .get("auth_token") - .ok_or(ChaosError::NotLoggedInError)?; + .ok_or(ChaosError::NotLoggedIn)?; let claims = decode_auth_token(token, decoding_key, jwt_validator) - .ok_or(ChaosError::NotLoggedInError)?; + .ok_or(ChaosError::NotLoggedIn)?; Ok(AuthUser { user_id: claims.sub, @@ -85,11 +85,11 @@ where let TypedHeader(cookies) = parts .extract::>() .await - .map_err(|_| ChaosError::NotLoggedInError)?; + .map_err(|_| ChaosError::NotLoggedIn)?; let token = cookies .get("auth_token") - .ok_or(ChaosError::NotLoggedInError)?; + .ok_or(ChaosError::NotLoggedIn)?; let claims = decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; @@ -128,14 +128,14 @@ where let TypedHeader(cookies) = parts .extract::>() .await - .map_err(|_| ChaosError::NotLoggedInError)?; + .map_err(|_| ChaosError::NotLoggedIn)?; let token = cookies .get("auth_token") - .ok_or(ChaosError::NotLoggedInError)?; + .ok_or(ChaosError::NotLoggedIn)?; let claims = decode_auth_token(token, decoding_key, jwt_validator) - .ok_or(ChaosError::NotLoggedInError)?; + .ok_or(ChaosError::NotLoggedIn)?; let pool = &app_state.db; let user_id = claims.sub; diff --git a/backend/server/src/service/organisation.rs b/backend/server/src/service/organisation.rs index d3865ca8..a29a1716 100644 --- a/backend/server/src/service/organisation.rs +++ b/backend/server/src/service/organisation.rs @@ -21,7 +21,7 @@ pub async fn user_is_admin( .await?.exists.expect("`exists` should always exist in this query result"); if !is_admin { - return Err(ChaosError::UnauthorizedError); + return Err(ChaosError::Unauthorized); } Ok(()) From 41388e43696b53166ab2c36787510710a9eb4b81 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 17:04:19 +1000 Subject: [PATCH 35/36] add routes to `main.rs` --- backend/server/src/handler/organisation.rs | 51 +++++++++++++--------- backend/server/src/main.rs | 9 +++- backend/server/src/models/organisation.rs | 24 +++++++++- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index a6c189b1..44747e6a 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -33,38 +33,48 @@ impl OrganisationHandler { pub async fn get( State(state): State, - Path(organisation_id): Path, + Path(id): Path, _user: AuthUser, ) -> Result { - let org = Organisation::get(organisation_id, &state.db).await?; + let org = Organisation::get(id, &state.db).await?; Ok((StatusCode::OK, Json(org))) } pub async fn delete( State(state): State, - Path(organisation_id): Path, + Path(id): Path, _user: SuperUser, ) -> Result { - Organisation::delete(organisation_id, &state.db).await?; + Organisation::delete(id, &state.db).await?; Ok((StatusCode::OK, "Successfully deleted organisation")) } + pub async fn get_admins( + State(state): State, + Path(id): Path, + _user: SuperUser + ) -> Result { + let members = Organisation::get_admins(id, &state.db).await?; + Ok((StatusCode::OK, Json(members))) + } + pub async fn get_members( State(state): State, - Path(organisation_id): Path, + Path(id): Path, _admin: OrganisationAdmin, ) -> Result { - let members = Organisation::get_members(organisation_id, &state.db).await?; + let members = Organisation::get_members(id, &state.db).await?; Ok((StatusCode::OK, Json(members))) } pub async fn update_admins( + State(state): State, + Path(id): Path, + _super_user: SuperUser, mut transaction: DBTransaction<'_>, - Path(organisation_id): Path, Json(request_body): Json, - _super_user: SuperUser, ) -> Result { - Organisation::update_admins(organisation_id, request_body.members, &mut transaction.tx) + Organisation::update_admins(id, request_body.members, &mut transaction.tx) .await?; transaction.tx.commit().await?; @@ -72,12 +82,13 @@ impl OrganisationHandler { } pub async fn update_members( + State(state): State, mut transaction: DBTransaction<'_>, - Path(organisation_id): Path, - Json(request_body): Json, + Path(id): Path, _admin: OrganisationAdmin, + Json(request_body): Json, ) -> Result { - Organisation::update_members(organisation_id, request_body.members, &mut transaction.tx) + Organisation::update_members(id, request_body.members, &mut transaction.tx) .await?; transaction.tx.commit().await?; @@ -86,11 +97,11 @@ impl OrganisationHandler { pub async fn remove_admin( State(state): State, - Path(organisation_id): Path, + Path(id): Path, _super_user: SuperUser, Json(request_body): Json, ) -> Result { - Organisation::remove_admin(organisation_id, request_body.user_id, &state.db).await?; + Organisation::remove_admin(id, request_body.user_id, &state.db).await?; Ok(( StatusCode::OK, @@ -100,11 +111,11 @@ impl OrganisationHandler { pub async fn remove_member( State(state): State, - Path(organisation_id): Path, + Path(id): Path, _admin: OrganisationAdmin, Json(request_body): Json, ) -> Result { - Organisation::remove_member(organisation_id, request_body.user_id, &state.db).await?; + Organisation::remove_member(id, request_body.user_id, &state.db).await?; Ok(( StatusCode::OK, @@ -114,20 +125,20 @@ impl OrganisationHandler { pub async fn update_logo( State(state): State, - Path(organisation_id): Path, + Path(id): Path, _admin: OrganisationAdmin, ) -> Result { let logo_url = - Organisation::update_logo(organisation_id, &state.db, &state.storage_bucket).await?; + Organisation::update_logo(id, &state.db, &state.storage_bucket).await?; Ok((StatusCode::OK, Json(logo_url))) } pub async fn get_campaigns( State(state): State, - Path(organisation_id): Path, + Path(id): Path, _user: AuthUser, ) -> Result { - let campaigns = Organisation::get_campaigns(organisation_id, &state.db).await?; + let campaigns = Organisation::get_campaigns(id, &state.db).await?; Ok((StatusCode::OK, Json(campaigns))) } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 52d544df..6e0cca21 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -2,8 +2,8 @@ use crate::handler::auth::google_callback; use crate::handler::organisation::OrganisationHandler; use crate::models::storage::Storage; use anyhow::Result; -use axum::routing::post; -use axum::{routing::get, Router}; +use axum::routing::{get, patch, post, put}; +use axum::{Router}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; @@ -65,6 +65,11 @@ async fn main() -> Result<()> { .route("/", get(|| async { "Hello, World!" })) .route("/api/auth/callback/google", get(google_callback)) .route("/api/v1/organisation", post(OrganisationHandler::create)) + .route("/api/v1/organisation/:id", get(OrganisationHandler::get).delete(OrganisationHandler::delete)) + .route("/api/v1/organisation/:id/campaign", get(OrganisationHandler::get_campaigns).post(OrganisationHandler::create_campaign)) + .route("/api/v1/organisation/:id/logo", patch(OrganisationHandler::update_logo)) + .route("/api/v1/organisation/:id/member", get(OrganisationHandler::get_members).put(OrganisationHandler::update_members).delete(OrganisationHandler::remove_member)) + .route("/api/v1/organisation/:id/admin", get(OrganisationHandler::get_admins).put(OrganisationHandler::update_admins).delete(OrganisationHandler::remove_admin)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 8c7f201e..d21bd50d 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -127,6 +127,28 @@ impl Organisation { Ok(()) } + pub async fn get_admins( + organisation_id: i64, + pool: &Pool, + ) -> Result { + let admin_list = sqlx::query_as!( + Member, + " + SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members + LEFT JOIN users on users.id = organisation_members.user_id + WHERE organisation_members.organisation_id = $1 AND organisation_members.role = $2 + ", + organisation_id, + OrganisationRole::Admin as OrganisationRole + ) + .fetch_all(pool) + .await?; + + Ok(MemberList { + members: admin_list, + }) + } + pub async fn get_members( organisation_id: i64, pool: &Pool, @@ -136,7 +158,7 @@ impl Organisation { " SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members LEFT JOIN users on users.id = organisation_members.user_id - WHERE organisation_id = $1 + WHERE organisation_members.organisation_id = $1 ", organisation_id ) From 8d856a7c3ee510e7bb2a7729a25ad5435eb53f73 Mon Sep 17 00:00:00 2001 From: Kavika Date: Fri, 28 Jun 2024 17:41:35 +1000 Subject: [PATCH 36/36] cargo fmt --- backend/server/src/handler/organisation.rs | 11 +++----- backend/server/src/main.rs | 31 +++++++++++++++++----- backend/server/src/models/auth.rs | 22 ++++++--------- backend/server/src/models/error.rs | 8 +++--- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 44747e6a..e1f08100 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -52,7 +52,7 @@ impl OrganisationHandler { pub async fn get_admins( State(state): State, Path(id): Path, - _user: SuperUser + _user: SuperUser, ) -> Result { let members = Organisation::get_admins(id, &state.db).await?; Ok((StatusCode::OK, Json(members))) @@ -74,8 +74,7 @@ impl OrganisationHandler { mut transaction: DBTransaction<'_>, Json(request_body): Json, ) -> Result { - Organisation::update_admins(id, request_body.members, &mut transaction.tx) - .await?; + Organisation::update_admins(id, request_body.members, &mut transaction.tx).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully updated organisation members")) @@ -88,8 +87,7 @@ impl OrganisationHandler { _admin: OrganisationAdmin, Json(request_body): Json, ) -> Result { - Organisation::update_members(id, request_body.members, &mut transaction.tx) - .await?; + Organisation::update_members(id, request_body.members, &mut transaction.tx).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully updated organisation members")) @@ -128,8 +126,7 @@ impl OrganisationHandler { Path(id): Path, _admin: OrganisationAdmin, ) -> Result { - let logo_url = - Organisation::update_logo(id, &state.db, &state.storage_bucket).await?; + let logo_url = Organisation::update_logo(id, &state.db, &state.storage_bucket).await?; Ok((StatusCode::OK, Json(logo_url))) } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 6e0cca21..38b2022c 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -3,7 +3,7 @@ use crate::handler::organisation::OrganisationHandler; use crate::models::storage::Storage; use anyhow::Result; use axum::routing::{get, patch, post, put}; -use axum::{Router}; +use axum::Router; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use models::app::AppState; use snowflake::SnowflakeIdGenerator; @@ -65,11 +65,30 @@ async fn main() -> Result<()> { .route("/", get(|| async { "Hello, World!" })) .route("/api/auth/callback/google", get(google_callback)) .route("/api/v1/organisation", post(OrganisationHandler::create)) - .route("/api/v1/organisation/:id", get(OrganisationHandler::get).delete(OrganisationHandler::delete)) - .route("/api/v1/organisation/:id/campaign", get(OrganisationHandler::get_campaigns).post(OrganisationHandler::create_campaign)) - .route("/api/v1/organisation/:id/logo", patch(OrganisationHandler::update_logo)) - .route("/api/v1/organisation/:id/member", get(OrganisationHandler::get_members).put(OrganisationHandler::update_members).delete(OrganisationHandler::remove_member)) - .route("/api/v1/organisation/:id/admin", get(OrganisationHandler::get_admins).put(OrganisationHandler::update_admins).delete(OrganisationHandler::remove_admin)) + .route( + "/api/v1/organisation/:id", + get(OrganisationHandler::get).delete(OrganisationHandler::delete), + ) + .route( + "/api/v1/organisation/:id/campaign", + get(OrganisationHandler::get_campaigns).post(OrganisationHandler::create_campaign), + ) + .route( + "/api/v1/organisation/:id/logo", + patch(OrganisationHandler::update_logo), + ) + .route( + "/api/v1/organisation/:id/member", + get(OrganisationHandler::get_members) + .put(OrganisationHandler::update_members) + .delete(OrganisationHandler::remove_member), + ) + .route( + "/api/v1/organisation/:id/admin", + get(OrganisationHandler::get_admins) + .put(OrganisationHandler::update_admins) + .delete(OrganisationHandler::remove_admin), + ) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index 17d18e31..b5e9c298 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -52,12 +52,10 @@ where .await .map_err(|_| ChaosError::NotLoggedIn)?; - let token = cookies - .get("auth_token") - .ok_or(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 claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; Ok(AuthUser { user_id: claims.sub, @@ -87,9 +85,7 @@ where .await .map_err(|_| ChaosError::NotLoggedIn)?; - let token = cookies - .get("auth_token") - .ok_or(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)?; @@ -104,7 +100,7 @@ where }); } } - + Err(ChaosError::Unauthorized) } } @@ -130,12 +126,10 @@ where .await .map_err(|_| ChaosError::NotLoggedIn)?; - let token = cookies - .get("auth_token") - .ok_or(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 claims = + decode_auth_token(token, decoding_key, jwt_validator).ok_or(ChaosError::NotLoggedIn)?; let pool = &app_state.db; let user_id = claims.sub; diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index cb25b95b..3a2869e3 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -15,7 +15,7 @@ pub enum ChaosError { #[error("Forbidden operation")] ForbiddenOperation, - + #[error("Bad request")] BadRequest, @@ -44,14 +44,16 @@ impl IntoResponse for ChaosError { match self { ChaosError::NotLoggedIn => Redirect::temporary("/auth/google").into_response(), ChaosError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), - ChaosError::ForbiddenOperation => (StatusCode::FORBIDDEN, "Forbidden operation").into_response(), + ChaosError::ForbiddenOperation => { + (StatusCode::FORBIDDEN, "Forbidden operation").into_response() + } ChaosError::BadRequest => (StatusCode::BAD_REQUEST, "Bad request").into_response(), ChaosError::DatabaseError(db_error) => match db_error { // We only care about the RowNotFound error, as others are miscellaneous DB errors. sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Not found").into_response(), _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response(), }, - _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() + _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response(), } } }