Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Organisations CRUD #485

Merged
merged 39 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9a36bfa
feat: Initial draft
Mar 30, 2024
dd3c390
rearranged import
Mar 30, 2024
4316158
clear errors
Mar 30, 2024
cd0d8da
post testing
Mar 31, 2024
0740566
feat(db): add indexes for foreign key reference columns
KavikaPalletenne Apr 7, 2024
cadc6c6
fix(db): index naming and missing semicolons
KavikaPalletenne Apr 7, 2024
3c397bf
dep(backend): update axum
KavikaPalletenne Apr 7, 2024
120f5e1
fix(backend): remove testing jwt handlers
KavikaPalletenne Apr 7, 2024
5c1c462
feat(backend): Add custom jwt validator and header
KavikaPalletenne Apr 7, 2024
156f67c
feat(backend): basic error handling enum
KavikaPalletenne Apr 7, 2024
a7483bc
fix(backend): ran cargo fmt
KavikaPalletenne Apr 7, 2024
25f43ea
fix(backend): remove unused imports
KavikaPalletenne Apr 7, 2024
2f26dbf
CRUD operations - awaiting Campaign - haven't enforced db safety
Apr 11, 2024
f61e1df
implement feedback
Jun 5, 2024
2068919
Update rust.yml to include 224 branch
KavikaPalletenne Jun 7, 2024
5604d74
logic and style fixes
KavikaPalletenne Jun 21, 2024
4236b27
Change to using `thiserror`
KavikaPalletenne Jun 21, 2024
afe948d
Merge branch 'CHAOS-224-KHAOS-rewrite' into CHAOS-473-error-handling
KavikaPalletenne Jun 21, 2024
dd57d70
Merge branch 'refs/heads/CHAOS-473-error-handling' into CHAOS-462-Org…
KavikaPalletenne Jun 21, 2024
f0bd677
add organisation_role type to db
KavikaPalletenne Jun 21, 2024
7468b16
update migration timestamps to be `NOT NULL`
KavikaPalletenne Jun 21, 2024
d7dffef
change sqlx `time` to `chrono`
KavikaPalletenne Jun 21, 2024
d52f3e4
integrate organisations crud with error handling
KavikaPalletenne Jun 21, 2024
bcb6be8
return member role with org members
KavikaPalletenne Jun 21, 2024
2b11ecc
update sqlx type name for `UserRole`
KavikaPalletenne Jun 21, 2024
fdebc28
simplify handlers to use new error type
KavikaPalletenne Jun 21, 2024
417e642
removed unused imports
KavikaPalletenne Jun 21, 2024
fa88d4e
added `OrganisationAdmin` extractor
KavikaPalletenne Jun 26, 2024
a472be2
use `Transaction` when doing multiple queries
KavikaPalletenne Jun 28, 2024
d201406
cargo fmt
KavikaPalletenne Jun 28, 2024
d3e31ac
add org route to app
KavikaPalletenne Jun 28, 2024
ddecb47
move `Organisation` service functions into `Organisation` struct
KavikaPalletenne Jun 28, 2024
a00ce1e
moved org handlers into `OrganisationHandler` struct
KavikaPalletenne Jun 28, 2024
f673213
ran cargo fmt
KavikaPalletenne Jun 28, 2024
90702f6
add S3 url generation to logo update
KavikaPalletenne Jun 28, 2024
0e57add
Merge branch 'CHAOS-224-KHAOS-rewrite' into CHAOS-462-Organisations-CRUD
KavikaPalletenne Jun 28, 2024
0b9f028
fixed error renaming
KavikaPalletenne Jun 28, 2024
41388e4
add routes to `main.rs`
KavikaPalletenne Jun 28, 2024
8d856a7
cargo fmt
KavikaPalletenne Jun 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading