Skip to content

Commit

Permalink
Organisations CRUD (#485)
Browse files Browse the repository at this point in the history
* feat: Initial draft
TODO: TESTING + more clarification

* rearranged import

* clear errors

* post testing

* feat(db): add indexes for foreign key reference columns

* fix(db): index naming and missing semicolons

* dep(backend): update axum

* fix(backend): remove testing jwt handlers

* feat(backend): Add custom jwt validator and header

* feat(backend): basic error handling enum

* fix(backend): ran cargo fmt

* fix(backend): remove unused imports

* CRUD operations - awaiting Campaign - haven't enforced db safety

* implement feedback

* Update rust.yml to include 224 branch

* logic and style fixes

* Change to using `thiserror`

* add organisation_role type to db

* update migration timestamps to be `NOT NULL`

* change sqlx `time` to `chrono`

* integrate organisations crud with error handling

* return member role with org members

* update sqlx type name for `UserRole`

* simplify handlers to use new error type

* removed unused imports

* added `OrganisationAdmin` extractor

* use `Transaction` when doing multiple queries

* cargo fmt

* add org route to app

* move `Organisation` service functions into `Organisation` struct

* moved org handlers into `OrganisationHandler` struct

* ran cargo fmt

* add S3 url generation to logo update

* fixed error renaming

* add routes to `main.rs`

* cargo fmt

---------

Co-authored-by: Alexander <[email protected]>
Co-authored-by: Kavika <[email protected]>
  • Loading branch information
3 people authored Jun 28, 2024
1 parent 1405e97 commit 5fe7add
Show file tree
Hide file tree
Showing 24 changed files with 762 additions and 31 deletions.
4 changes: 4 additions & 0 deletions backend/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,10 @@
"name": {
"type": "string",
"example": "Clancy Lion"
},
"role": {
"type": "string",
"example": "Admin"
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions backend/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions backend/migrations/20240406023149_create_users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
13 changes: 8 additions & 5 deletions backend/migrations/20240406024211_create_organisations.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
8 changes: 4 additions & 4 deletions backend/migrations/20240406025537_create_campaigns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions backend/migrations/20240406031400_create_questions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions backend/migrations/20240406031915_create_applications.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion backend/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -22,6 +22,8 @@ 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"

1 change: 1 addition & 0 deletions backend/server/src/handler/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod auth;
pub mod organisation;
160 changes: 160 additions & 0 deletions backend/server/src/handler/organisation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use crate::models;
use crate::models::app::AppState;
use crate::models::auth::SuperUser;
use crate::models::auth::{AuthUser, OrganisationAdmin};
use crate::models::error::ChaosError;
use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation};
use crate::models::transaction::DBTransaction;
use crate::service;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;

pub struct OrganisationHandler;

impl OrganisationHandler {
pub async fn create(
State(state): State<AppState>,
_user: SuperUser,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewOrganisation>,
) -> Result<impl IntoResponse, ChaosError> {
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<AppState>,
Path(id): Path<i64>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let org = Organisation::get(id, &state.db).await?;
Ok((StatusCode::OK, Json(org)))
}

pub async fn delete(
State(state): State<AppState>,
Path(id): Path<i64>,
_user: SuperUser,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::delete(id, &state.db).await?;
Ok((StatusCode::OK, "Successfully deleted organisation"))
}

pub async fn get_admins(
State(state): State<AppState>,
Path(id): Path<i64>,
_user: SuperUser,
) -> Result<impl IntoResponse, ChaosError> {
let members = Organisation::get_admins(id, &state.db).await?;
Ok((StatusCode::OK, Json(members)))
}

pub async fn get_members(
State(state): State<AppState>,
Path(id): Path<i64>,
_admin: OrganisationAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let members = Organisation::get_members(id, &state.db).await?;
Ok((StatusCode::OK, Json(members)))
}

pub async fn update_admins(
State(state): State<AppState>,
Path(id): Path<i64>,
_super_user: SuperUser,
mut transaction: DBTransaction<'_>,
Json(request_body): Json<AdminUpdateList>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::update_admins(id, request_body.members, &mut transaction.tx).await?;

transaction.tx.commit().await?;
Ok((StatusCode::OK, "Successfully updated organisation members"))
}

pub async fn update_members(
State(state): State<AppState>,
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_admin: OrganisationAdmin,
Json(request_body): Json<AdminUpdateList>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::update_members(id, request_body.members, &mut transaction.tx).await?;

transaction.tx.commit().await?;
Ok((StatusCode::OK, "Successfully updated organisation members"))
}

pub async fn remove_admin(
State(state): State<AppState>,
Path(id): Path<i64>,
_super_user: SuperUser,
Json(request_body): Json<AdminToRemove>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::remove_admin(id, request_body.user_id, &state.db).await?;

Ok((
StatusCode::OK,
"Successfully removed member from organisation",
))
}

pub async fn remove_member(
State(state): State<AppState>,
Path(id): Path<i64>,
_admin: OrganisationAdmin,
Json(request_body): Json<AdminToRemove>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::remove_member(id, request_body.user_id, &state.db).await?;

Ok((
StatusCode::OK,
"Successfully removed member from organisation",
))
}

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

pub async fn get_campaigns(
State(state): State<AppState>,
Path(id): Path<i64>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let campaigns = Organisation::get_campaigns(id, &state.db).await?;

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

pub async fn create_campaign(
State(mut state): State<AppState>,
_admin: OrganisationAdmin,
Json(request_body): Json<models::campaign::Campaign>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::create_campaign(
request_body.name,
request_body.description,
request_body.starts_at,
request_body.ends_at,
&state.db,
&mut state.snowflake_generator,
)
.await?;

Ok((StatusCode::OK, "Successfully created campaign"))
}
}
36 changes: 34 additions & 2 deletions backend/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::handler::auth::google_callback;
use crate::handler::organisation::OrganisationHandler;
use crate::models::storage::Storage;
use anyhow::Result;
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;
use sqlx::postgres::PgPoolOptions;
use std::env;
use crate::handler::auth::google_callback;

mod handler;
mod models;
Expand Down Expand Up @@ -43,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,
Expand All @@ -52,11 +58,37 @@ async fn main() -> Result<()> {
jwt_header,
jwt_validator,
snowflake_generator,
storage_bucket,
};

let app = Router::new()
.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();
Expand Down
2 changes: 2 additions & 0 deletions backend/server/src/models/app.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -12,4 +13,5 @@ pub struct AppState {
pub jwt_header: Header,
pub jwt_validator: Validation,
pub snowflake_generator: SnowflakeIdGenerator,
pub storage_bucket: Bucket,
}
Loading

0 comments on commit 5fe7add

Please sign in to comment.