From 5db09ade64199740c7d154c6bed944f900f6332e Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 2 Dec 2024 23:28:35 +1100 Subject: [PATCH 01/20] email sending --- .../20241124054711_email_templates.sql | 3 +- backend/server/Cargo.toml | 1 + backend/server/src/handler/offer.rs | 6 +- backend/server/src/models/app.rs | 6 ++ backend/server/src/models/email.rs | 56 +++++++++++++++++++ backend/server/src/models/email_template.rs | 27 ++++++--- backend/server/src/models/error.rs | 9 +++ backend/server/src/models/mod.rs | 1 + backend/server/src/models/offer.rs | 13 +++-- backend/setup-dev-env.sh | 3 + 10 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 backend/server/src/models/email.rs diff --git a/backend/migrations/20241124054711_email_templates.sql b/backend/migrations/20241124054711_email_templates.sql index 393123c9..ff1a4879 100644 --- a/backend/migrations/20241124054711_email_templates.sql +++ b/backend/migrations/20241124054711_email_templates.sql @@ -2,7 +2,8 @@ CREATE TABLE email_templates ( id BIGINT PRIMARY KEY, organisation_id BIGINT NOT NULL, name TEXT NOT NULL, - template TEXT NOT NULL, + template_subject TEXT NOT NULL, + template_body TEXT NOT NULL, CONSTRAINT FK_email_templates_organisations FOREIGN KEY(organisation_id) REFERENCES organisations(id) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index de3d9bb6..f3e4c07f 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -27,3 +27,4 @@ rs-snowflake = "0.6" jsonwebtoken = "9.1" dotenvy = "0.15" handlebars = "6.2" +lettre = { version = "0.11", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder"] } diff --git a/backend/server/src/handler/offer.rs b/backend/server/src/handler/offer.rs index 92297c63..b73e4438 100644 --- a/backend/server/src/handler/offer.rs +++ b/backend/server/src/handler/offer.rs @@ -2,9 +2,10 @@ use crate::models::auth::{OfferAdmin, OfferRecipient}; use crate::models::error::ChaosError; use crate::models::offer::{Offer, OfferReply}; use crate::models::transaction::DBTransaction; -use axum::extract::{Json, Path}; +use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; +use crate::models::app::AppState; pub struct OfferHandler; impl OfferHandler { @@ -57,8 +58,9 @@ impl OfferHandler { mut transaction: DBTransaction<'_>, Path(id): Path, _user: OfferAdmin, + State(state): State ) -> Result { - Offer::send_offer(id, &mut transaction.tx).await?; + Offer::send_offer(id, &mut transaction.tx, state.email_credentials).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully sent offer")) diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index 5fd59143..efebced5 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -20,6 +20,7 @@ use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use sqlx::{Pool, Postgres}; use std::env; +use crate::models::email::{ChaosEmail, EmailCredentials}; #[derive(Clone)] pub struct AppState { @@ -31,6 +32,7 @@ pub struct AppState { pub jwt_validator: Validation, pub snowflake_generator: SnowflakeIdGenerator, pub storage_bucket: Bucket, + pub email_credentials: EmailCredentials, } pub async fn app() -> Result { @@ -65,6 +67,9 @@ pub async fn app() -> Result { // Initialise S3 bucket let storage_bucket = Storage::init_bucket(); + // Initialise email credentials + let email_credentials = ChaosEmail::setup_credentials(); + // Add all data to AppState let state = AppState { db: pool, @@ -75,6 +80,7 @@ pub async fn app() -> Result { jwt_validator, snowflake_generator, storage_bucket, + email_credentials, }; Ok(Router::new() diff --git a/backend/server/src/models/email.rs b/backend/server/src/models/email.rs new file mode 100644 index 00000000..22a2df43 --- /dev/null +++ b/backend/server/src/models/email.rs @@ -0,0 +1,56 @@ +use std::env; +use lettre::{AsyncSmtpTransport, AsyncTransport, Message, SmtpTransport, Tokio1Executor, Transport}; +use lettre::transport::smtp::authentication::Credentials; +use crate::models::error::ChaosError; + +pub struct ChaosEmail; + +#[derive(Clone)] +pub struct EmailCredentials { + pub credentials: Credentials, + pub email_host: String, +} + +pub struct EmailParts { + pub subject: String, + pub body: String, +} + +impl ChaosEmail { + pub fn setup_credentials() -> EmailCredentials { + let smtp_username = env::var("SMTP_USERNAME") + .expect("Error getting SMTP USERNAME") + .to_string(); + + let smtp_password = env::var("SMTP_PASSWORD") + .expect("Error getting SMTP PASSWORD") + .to_string(); + + let email_host = env::var("SMTP_HOST") + .expect("Error getting SMTP HOST") + .to_string(); + + EmailCredentials { + credentials: Credentials::new(smtp_username, smtp_password), + email_host + } + } + + fn new_connection(credentials: EmailCredentials) -> Result, ChaosError> { + Ok(AsyncSmtpTransport::relay(&*credentials.email_host)?.credentials(credentials.credentials).build()) + } + + pub async fn send_message(recepient_name: String, recepient_email_address: String, subject: String, body: String, credentials: EmailCredentials) -> Result<(), ChaosError> { + let message = Message::builder() + .from("Chaos Subcommittee Recruitment ".parse()?) + .reply_to("help@chaos.devsoc.app".parse()?) + .to(format!("{recepient_name} <{recepient_email_address}>").parse()?) + .subject(subject) + .body(body)?; + + let mailer = Self::new_connection(credentials)?; + mailer.send(message).await?; + + Ok(()) + } +} \ No newline at end of file diff --git a/backend/server/src/models/email_template.rs b/backend/server/src/models/email_template.rs index dd91cf00..80680b03 100644 --- a/backend/server/src/models/email_template.rs +++ b/backend/server/src/models/email_template.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, Transaction}; use std::collections::HashMap; use std::ops::DerefMut; +use crate::models::email::EmailParts; /// Email templates to update applicants /// Supported tags: @@ -18,7 +19,8 @@ pub struct EmailTemplate { pub id: i64, pub organisation_id: i64, pub name: String, - pub template: String, + pub template_subject: String, + pub template_body: String, } impl EmailTemplate { @@ -55,16 +57,18 @@ impl EmailTemplate { pub async fn update( id: i64, name: String, - template: String, + template_subject: String, + template_body: String, pool: &Pool, ) -> Result<(), ChaosError> { let _ = sqlx::query!( " - UPDATE email_templates SET name = $2, template = $3 WHERE id = $1 RETURNING id + UPDATE email_templates SET name = $2, template_subject = $3, template_body = $4 WHERE id = $1 RETURNING id ", id, name, - template + template_subject, + template_body ) .fetch_one(pool) .await?; @@ -88,11 +92,12 @@ impl EmailTemplate { expiry_date: DateTime, email_template_id: i64, transaction: &mut Transaction<'_, Postgres>, - ) -> Result { + ) -> Result { let template = EmailTemplate::get(email_template_id, transaction).await?; let mut handlebars = Handlebars::new(); - handlebars.register_template_string("template", template.template)?; + handlebars.register_template_string("template_subject", template.template_subject)?; + handlebars.register_template_string("template_body", template.template_body)?; let mut data = HashMap::new(); data.insert("name", name); @@ -107,8 +112,14 @@ impl EmailTemplate { .to_string(), ); - let final_string = handlebars.render("template", &data)?; + let subject = handlebars.render("template_subject", &data)?; + let body = handlebars.render("template_body", &data)?; - Ok(final_string) + Ok( + EmailParts { + subject, + body + } + ) } } diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 25f3f794..734ca8ba 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -45,6 +45,15 @@ pub enum ChaosError { #[error("Template rendering error")] TemplateRendorError(#[from] handlebars::RenderError), + + #[error("Lettre error")] + LettreError(#[from] lettre::error::Error), + + #[error("Email address error")] + AddressError(#[from] lettre::address::AddressError), + + #[error("SMTP transport error")] + SmtpTransportError(#[from] lettre::transport::smtp::Error), } /// 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 e2d03ca5..233f1981 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -13,3 +13,4 @@ pub mod role; pub mod storage; pub mod transaction; pub mod user; +pub mod email; diff --git a/backend/server/src/models/offer.rs b/backend/server/src/models/offer.rs index d0201756..25c86b45 100644 --- a/backend/server/src/models/offer.rs +++ b/backend/server/src/models/offer.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{Postgres, Transaction}; use std::ops::DerefMut; +use crate::models::email::{ChaosEmail, EmailCredentials, EmailParts}; #[derive(Deserialize)] pub struct Offer { @@ -186,9 +187,9 @@ impl Offer { pub async fn preview_email( id: i64, transaction: &mut Transaction<'_, Postgres>, - ) -> Result { + ) -> Result { let offer = Offer::get(id, transaction).await?; - let email = EmailTemplate::generate_email( + let email_parts = EmailTemplate::generate_email( offer.user_name, offer.role_name, offer.organisation_name, @@ -198,15 +199,17 @@ impl Offer { transaction, ) .await?; - Ok(email) + + Ok(email_parts) } pub async fn send_offer( id: i64, transaction: &mut Transaction<'_, Postgres>, + email_credentials: EmailCredentials, ) -> Result<(), ChaosError> { let offer = Offer::get(id, transaction).await?; - let email = EmailTemplate::generate_email( + let email_parts = EmailTemplate::generate_email( offer.user_name, offer.role_name, offer.organisation_name, @@ -217,7 +220,7 @@ impl Offer { ) .await?; - // TODO: Send email e.g. send_email(offer.user_email, email).await?; + ChaosEmail::send_message(offer.user_name, offer.user_email, email_parts.subject, email_parts.body, email_credentials).await?; Ok(()) } } diff --git a/backend/setup-dev-env.sh b/backend/setup-dev-env.sh index 0eaa03a0..f0f22268 100755 --- a/backend/setup-dev-env.sh +++ b/backend/setup-dev-env.sh @@ -19,6 +19,9 @@ S3_ACCESS_KEY="test_access_key" S3_SECRET_KEY="test_secret_key" S3_ENDPOINT="https://chaos-storage.s3.ap-southeast-1.amazonaws.com" S3_REGION_NAME="ap-southeast-1" +SMTP_USERNAME="test_username" +SMTP_PASSWORD="test_password" +SMTP_HOST="smtp.example.com" EOF # Check the user has all required tools installed. From 5a163d3399a10227cd905060cf709b9ee216b5c7 Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 2 Dec 2024 23:29:39 +1100 Subject: [PATCH 02/20] fix "recipient" spelling --- backend/server/src/models/email.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server/src/models/email.rs b/backend/server/src/models/email.rs index 22a2df43..023ff516 100644 --- a/backend/server/src/models/email.rs +++ b/backend/server/src/models/email.rs @@ -40,11 +40,11 @@ impl ChaosEmail { Ok(AsyncSmtpTransport::relay(&*credentials.email_host)?.credentials(credentials.credentials).build()) } - pub async fn send_message(recepient_name: String, recepient_email_address: String, subject: String, body: String, credentials: EmailCredentials) -> Result<(), ChaosError> { + pub async fn send_message(recipient_name: String, recipient_email_address: String, subject: String, body: String, credentials: EmailCredentials) -> Result<(), ChaosError> { let message = Message::builder() .from("Chaos Subcommittee Recruitment ".parse()?) .reply_to("help@chaos.devsoc.app".parse()?) - .to(format!("{recepient_name} <{recepient_email_address}>").parse()?) + .to(format!("{recipient_name} <{recipient_email_address}>").parse()?) .subject(subject) .body(body)?; From 07680ccbc9e4ab82c7b836b2c038900dd951a9aa Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 2 Dec 2024 23:29:56 +1100 Subject: [PATCH 03/20] cargo fmt --- backend/server/src/handler/offer.rs | 4 +-- backend/server/src/models/app.rs | 2 +- backend/server/src/models/email.rs | 28 +++++++++++++++------ backend/server/src/models/email_template.rs | 9 ++----- backend/server/src/models/mod.rs | 2 +- backend/server/src/models/offer.rs | 11 ++++++-- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/backend/server/src/handler/offer.rs b/backend/server/src/handler/offer.rs index b73e4438..09ea3472 100644 --- a/backend/server/src/handler/offer.rs +++ b/backend/server/src/handler/offer.rs @@ -1,3 +1,4 @@ +use crate::models::app::AppState; use crate::models::auth::{OfferAdmin, OfferRecipient}; use crate::models::error::ChaosError; use crate::models::offer::{Offer, OfferReply}; @@ -5,7 +6,6 @@ use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; -use crate::models::app::AppState; pub struct OfferHandler; impl OfferHandler { @@ -58,7 +58,7 @@ impl OfferHandler { mut transaction: DBTransaction<'_>, Path(id): Path, _user: OfferAdmin, - State(state): State + State(state): State, ) -> Result { Offer::send_offer(id, &mut transaction.tx, state.email_credentials).await?; transaction.tx.commit().await?; diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index efebced5..f4114db8 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -9,6 +9,7 @@ use crate::handler::question::QuestionHandler; use crate::handler::rating::RatingHandler; use crate::handler::role::RoleHandler; use crate::handler::user::UserHandler; +use crate::models::email::{ChaosEmail, EmailCredentials}; use crate::models::error::ChaosError; use crate::models::storage::Storage; use axum::routing::{get, patch, post}; @@ -20,7 +21,6 @@ use snowflake::SnowflakeIdGenerator; use sqlx::postgres::PgPoolOptions; use sqlx::{Pool, Postgres}; use std::env; -use crate::models::email::{ChaosEmail, EmailCredentials}; #[derive(Clone)] pub struct AppState { diff --git a/backend/server/src/models/email.rs b/backend/server/src/models/email.rs index 023ff516..b42d843a 100644 --- a/backend/server/src/models/email.rs +++ b/backend/server/src/models/email.rs @@ -1,7 +1,9 @@ -use std::env; -use lettre::{AsyncSmtpTransport, AsyncTransport, Message, SmtpTransport, Tokio1Executor, Transport}; -use lettre::transport::smtp::authentication::Credentials; use crate::models::error::ChaosError; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{ + AsyncSmtpTransport, AsyncTransport, Message, SmtpTransport, Tokio1Executor, Transport, +}; +use std::env; pub struct ChaosEmail; @@ -32,15 +34,25 @@ impl ChaosEmail { EmailCredentials { credentials: Credentials::new(smtp_username, smtp_password), - email_host + email_host, } } - fn new_connection(credentials: EmailCredentials) -> Result, ChaosError> { - Ok(AsyncSmtpTransport::relay(&*credentials.email_host)?.credentials(credentials.credentials).build()) + fn new_connection( + credentials: EmailCredentials, + ) -> Result, ChaosError> { + Ok(AsyncSmtpTransport::relay(&*credentials.email_host)? + .credentials(credentials.credentials) + .build()) } - pub async fn send_message(recipient_name: String, recipient_email_address: String, subject: String, body: String, credentials: EmailCredentials) -> Result<(), ChaosError> { + pub async fn send_message( + recipient_name: String, + recipient_email_address: String, + subject: String, + body: String, + credentials: EmailCredentials, + ) -> Result<(), ChaosError> { let message = Message::builder() .from("Chaos Subcommittee Recruitment ".parse()?) .reply_to("help@chaos.devsoc.app".parse()?) @@ -53,4 +65,4 @@ impl ChaosEmail { Ok(()) } -} \ No newline at end of file +} diff --git a/backend/server/src/models/email_template.rs b/backend/server/src/models/email_template.rs index 80680b03..ecf3792e 100644 --- a/backend/server/src/models/email_template.rs +++ b/backend/server/src/models/email_template.rs @@ -1,3 +1,4 @@ +use crate::models::email::EmailParts; use crate::models::error::ChaosError; use chrono::{DateTime, Local, Utc}; use handlebars::Handlebars; @@ -5,7 +6,6 @@ use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, Transaction}; use std::collections::HashMap; use std::ops::DerefMut; -use crate::models::email::EmailParts; /// Email templates to update applicants /// Supported tags: @@ -115,11 +115,6 @@ impl EmailTemplate { let subject = handlebars.render("template_subject", &data)?; let body = handlebars.render("template_body", &data)?; - Ok( - EmailParts { - subject, - body - } - ) + Ok(EmailParts { subject, body }) } } diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index 233f1981..d1922411 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -3,6 +3,7 @@ pub mod app; pub mod application; pub mod auth; pub mod campaign; +pub mod email; pub mod email_template; pub mod error; pub mod offer; @@ -13,4 +14,3 @@ pub mod role; pub mod storage; pub mod transaction; pub mod user; -pub mod email; diff --git a/backend/server/src/models/offer.rs b/backend/server/src/models/offer.rs index 25c86b45..58abdae0 100644 --- a/backend/server/src/models/offer.rs +++ b/backend/server/src/models/offer.rs @@ -1,3 +1,4 @@ +use crate::models::email::{ChaosEmail, EmailCredentials, EmailParts}; use crate::models::email_template::EmailTemplate; use crate::models::error::ChaosError; use chrono::{DateTime, Utc}; @@ -5,7 +6,6 @@ use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{Postgres, Transaction}; use std::ops::DerefMut; -use crate::models::email::{ChaosEmail, EmailCredentials, EmailParts}; #[derive(Deserialize)] pub struct Offer { @@ -220,7 +220,14 @@ impl Offer { ) .await?; - ChaosEmail::send_message(offer.user_name, offer.user_email, email_parts.subject, email_parts.body, email_credentials).await?; + ChaosEmail::send_message( + offer.user_name, + offer.user_email, + email_parts.subject, + email_parts.body, + email_credentials, + ) + .await?; Ok(()) } } From 7c3d62452a622a4867d30df0522188b0535e9582 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 00:05:09 +1100 Subject: [PATCH 04/20] fix errors with `template_subject` introduction --- backend/server/src/handler/email_template.rs | 9 ++++++++- backend/server/src/handler/offer.rs | 4 ++-- backend/server/src/handler/organisation.rs | 5 +++-- backend/server/src/models/email.rs | 10 +++++++--- backend/server/src/models/offer.rs | 2 +- backend/server/src/models/organisation.rs | 10 ++++++---- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/backend/server/src/handler/email_template.rs b/backend/server/src/handler/email_template.rs index e392834f..4150ac98 100644 --- a/backend/server/src/handler/email_template.rs +++ b/backend/server/src/handler/email_template.rs @@ -25,7 +25,14 @@ impl EmailTemplateHandler { State(state): State, Json(request_body): Json, ) -> Result { - EmailTemplate::update(id, request_body.name, request_body.template, &state.db).await?; + EmailTemplate::update( + id, + request_body.name, + request_body.template_subject, + request_body.template_body, + &state.db, + ) + .await?; Ok((StatusCode::OK, "Successfully updated email template")) } diff --git a/backend/server/src/handler/offer.rs b/backend/server/src/handler/offer.rs index 09ea3472..32e4851c 100644 --- a/backend/server/src/handler/offer.rs +++ b/backend/server/src/handler/offer.rs @@ -48,10 +48,10 @@ impl OfferHandler { Path(id): Path, _user: OfferAdmin, ) -> Result { - let string = Offer::preview_email(id, &mut transaction.tx).await?; + let email_parts = Offer::preview_email(id, &mut transaction.tx).await?; transaction.tx.commit().await?; - Ok((StatusCode::OK, string)) + Ok((StatusCode::OK, Json(email_parts))) } pub async fn send_offer( diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 7950f1fb..f43db3c1 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -199,12 +199,13 @@ impl OrganisationHandler { Path(id): Path, State(state): State, _admin: OrganisationAdmin, - Json(request_body): Json, + Json(request_body): Json, ) -> Result { Organisation::create_email_template( id, request_body.name, - request_body.template, + request_body.template_subject, + request_body.template_body, &state.db, state.snowflake_generator, ) diff --git a/backend/server/src/models/email.rs b/backend/server/src/models/email.rs index b42d843a..72afffe5 100644 --- a/backend/server/src/models/email.rs +++ b/backend/server/src/models/email.rs @@ -3,6 +3,7 @@ use lettre::transport::smtp::authentication::Credentials; use lettre::{ AsyncSmtpTransport, AsyncTransport, Message, SmtpTransport, Tokio1Executor, Transport, }; +use serde::Serialize; use std::env; pub struct ChaosEmail; @@ -13,6 +14,7 @@ pub struct EmailCredentials { pub email_host: String, } +#[derive(Serialize)] pub struct EmailParts { pub subject: String, pub body: String, @@ -41,9 +43,11 @@ impl ChaosEmail { fn new_connection( credentials: EmailCredentials, ) -> Result, ChaosError> { - Ok(AsyncSmtpTransport::relay(&*credentials.email_host)? - .credentials(credentials.credentials) - .build()) + Ok( + AsyncSmtpTransport::::relay(&*credentials.email_host)? + .credentials(credentials.credentials) + .build(), + ) } pub async fn send_message( diff --git a/backend/server/src/models/offer.rs b/backend/server/src/models/offer.rs index 58abdae0..60fa94ee 100644 --- a/backend/server/src/models/offer.rs +++ b/backend/server/src/models/offer.rs @@ -210,7 +210,7 @@ impl Offer { ) -> Result<(), ChaosError> { let offer = Offer::get(id, transaction).await?; let email_parts = EmailTemplate::generate_email( - offer.user_name, + offer.user_name.clone(), offer.role_name, offer.organisation_name, offer.campaign_name, diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 94824c37..8942ad2f 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -439,7 +439,8 @@ impl Organisation { pub async fn create_email_template( organisation_id: i64, name: String, - template: String, + template_subject: String, + template_body: String, pool: &Pool, mut snowflake_generator: SnowflakeIdGenerator, ) -> Result { @@ -447,13 +448,14 @@ impl Organisation { let _ = sqlx::query!( " - INSERT INTO email_templates (id, organisation_id, name, template) - VALUES ($1, $2, $3, $4) + INSERT INTO email_templates (id, organisation_id, name, template_subject, template_body) + VALUES ($1, $2, $3, $4, $5) ", id, organisation_id, name, - template + template_subject, + template_body ) .execute(pool) .await?; From 5aeeb2fedb5941cb32e69e9e5978dc74e7c4e3d0 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 00:16:25 +1100 Subject: [PATCH 05/20] cargo clippy fixes --- backend/server/src/handler/organisation.rs | 1 - backend/server/src/models/answer.rs | 9 ++++----- backend/server/src/models/email.rs | 6 ++---- backend/server/src/models/question.rs | 17 ++++------------- backend/server/src/models/storage.rs | 2 +- 5 files changed, 11 insertions(+), 24 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index f43db3c1..313fabfd 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,4 +1,3 @@ -use crate::models; use crate::models::app::AppState; use crate::models::auth::SuperUser; use crate::models::auth::{AuthUser, OrganisationAdmin}; diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index c9f51772..3b5d2fb1 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -35,7 +35,6 @@ pub struct Answer { #[derive(Deserialize)] pub struct NewAnswer { - pub application_id: i64, pub question_id: i64, #[serde(flatten)] @@ -355,7 +354,7 @@ impl AnswerData { multi_option_answers: Option>, ranking_answers: Option>, ) -> Self { - return match question_type { + match question_type { QuestionType::ShortAnswer => { let answer = short_answer_answer.expect("Data should exist for ShortAnswer variant"); @@ -376,18 +375,18 @@ impl AnswerData { let options = ranking_answers.expect("Data should exist for Ranking variant"); AnswerData::Ranking(options) } - }; + } } pub fn validate(&self) -> Result<(), ChaosError> { match self { Self::ShortAnswer(text) => { - if text.len() == 0 { + if text.is_empty() { return Err(ChaosError::BadRequest); } } Self::MultiSelect(data) | Self::Ranking(data) => { - if data.len() == 0 { + if data.is_empty() { return Err(ChaosError::BadRequest); } } diff --git a/backend/server/src/models/email.rs b/backend/server/src/models/email.rs index 72afffe5..e36a090d 100644 --- a/backend/server/src/models/email.rs +++ b/backend/server/src/models/email.rs @@ -1,8 +1,6 @@ use crate::models::error::ChaosError; use lettre::transport::smtp::authentication::Credentials; -use lettre::{ - AsyncSmtpTransport, AsyncTransport, Message, SmtpTransport, Tokio1Executor, Transport, -}; +use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; use serde::Serialize; use std::env; @@ -44,7 +42,7 @@ impl ChaosEmail { credentials: EmailCredentials, ) -> Result, ChaosError> { Ok( - AsyncSmtpTransport::::relay(&*credentials.email_host)? + AsyncSmtpTransport::::relay(&credentials.email_host)? .credentials(credentials.credentials) .build(), ) diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index db1f70f3..86957454 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -491,20 +491,11 @@ impl QuestionType { } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Default)] pub struct MultiOptionData { options: Vec, } -impl Default for MultiOptionData { - fn default() -> Self { - Self { - // Return an empty vector to be replaced by real data later on. - options: vec![], - } - } -} - /// Each of these structs represent a row in the `multi_option_question_options` /// table. For a `MultiChoice` question like "What is your favourite programming /// language?", there would be rows for "Rust", "Java" and "TypeScript". @@ -530,7 +521,7 @@ impl QuestionData { question_type: QuestionType, multi_option_data: Option>>, ) -> Self { - return if question_type == QuestionType::ShortAnswer { + if question_type == QuestionType::ShortAnswer { QuestionData::ShortAnswer } else if question_type == QuestionType::MultiChoice || question_type == QuestionType::MultiSelect @@ -551,7 +542,7 @@ impl QuestionData { } } else { QuestionData::ShortAnswer // Should never be reached, hence return ShortAnswer - }; + } } pub fn validate(&self) -> Result<(), ChaosError> { @@ -561,7 +552,7 @@ impl QuestionData { | Self::MultiSelect(data) | Self::DropDown(data) | Self::Ranking(data) => { - if data.options.len() > 0 { + if !data.options.is_empty() { return Ok(()); }; diff --git a/backend/server/src/models/storage.rs b/backend/server/src/models/storage.rs index d44b35aa..f547d0a9 100644 --- a/backend/server/src/models/storage.rs +++ b/backend/server/src/models/storage.rs @@ -37,7 +37,7 @@ impl Storage { endpoint, }; - let bucket = Bucket::new(&*bucket_name, region, credentials).unwrap(); + let bucket = Bucket::new(&bucket_name, region, credentials).unwrap(); // TODO: Change depending on style used by provider // bucket.set_path_style(); From 706f81463db465a6b3f8016dffbc808e662ba9ba Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 00:35:55 +1100 Subject: [PATCH 06/20] application role preferences and updating applied roles --- .../20240406031915_create_applications.sql | 1 + backend/server/src/handler/application.rs | 15 ++++- backend/server/src/models/application.rs | 64 ++++++++++++++++--- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index 01ee6439..e767edde 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -24,6 +24,7 @@ CREATE TABLE application_roles ( id BIGSERIAL PRIMARY KEY, application_id BIGINT NOT NULL, campaign_role_id BIGINT NOT NULL, + preference INTEGER NOT NULL, CONSTRAINT FK_application_roles_applications FOREIGN KEY(application_id) REFERENCES applications(id) diff --git a/backend/server/src/handler/application.rs b/backend/server/src/handler/application.rs index 8f6c2ecf..3c84b3cd 100644 --- a/backend/server/src/handler/application.rs +++ b/backend/server/src/handler/application.rs @@ -1,6 +1,6 @@ use crate::models::app::AppState; -use crate::models::application::{Application, ApplicationStatus}; -use crate::models::auth::{ApplicationAdmin, AuthUser}; +use crate::models::application::{Application, ApplicationRoleUpdate, ApplicationStatus}; +use crate::models::auth::{ApplicationAdmin, ApplicationOwner, AuthUser}; use crate::models::error::ChaosError; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; @@ -48,4 +48,15 @@ impl ApplicationHandler { transaction.tx.commit().await?; Ok((StatusCode::OK, Json(applications))) } + + pub async fn update_roles( + _user: ApplicationOwner, + Path(application_id): Path, + mut transaction: DBTransaction<'_>, + Json(data): Json + ) -> Result { + Application::update_roles(application_id, data.roles, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully updated application roles")) + } } diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index 1cd9a74f..fd3d3df8 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -27,6 +27,7 @@ pub struct ApplicationRole { pub id: i64, pub application_id: i64, pub campaign_role_id: i64, + pub preference: i32, } #[derive(Deserialize, Serialize)] @@ -64,6 +65,12 @@ pub struct ApplicationData { pub struct ApplicationAppliedRoleDetails { pub campaign_role_id: i64, pub role_name: String, + pub preference: i32, +} + +#[derive(Deserialize)] +pub struct ApplicationRoleUpdate { + pub roles: Vec } #[derive(Deserialize, Serialize, sqlx::Type, Clone, Debug)] @@ -101,11 +108,12 @@ impl Application { for role_applied in application_data.applied_roles { sqlx::query!( " - INSERT INTO application_roles (application_id, campaign_role_id) - VALUES ($1, $2) + INSERT INTO application_roles (application_id, campaign_role_id, preference) + VALUES ($1, $2, $3) ", id, - role_applied.campaign_role_id + role_applied.campaign_role_id, + role_applied.preference ) .execute(transaction.deref_mut()) .await?; @@ -140,7 +148,8 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles LEFT JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id @@ -198,7 +207,8 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles LEFT JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id @@ -261,7 +271,8 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles LEFT JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id @@ -324,7 +335,8 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles LEFT JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id @@ -338,8 +350,9 @@ impl Application { let details = ApplicationDetails { id: application_data.id, campaign_id: application_data.campaign_id, - status: application_data.status, - private_status: application_data.private_status, + status: application_data.status.clone(), + // To reuse struct, do not show use private status + private_status: application_data.status, applied_roles, user: UserDetails { id: application_data.user_id, @@ -398,4 +411,37 @@ impl Application { Ok(()) } + + pub async fn update_roles( + id: i64, + roles: Vec, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + // There can be 0 roles, so we cannot use the `RETURNING id` method, + // as there could be 0 roles. If application_id is wrong, then + // the next query will error, preventing changes to DB. + sqlx::query!( + " + DELETE FROM application_roles WHERE application_id = $1 + ", + id + ) + .execute(transaction.deref_mut()) + .await?; + + // Insert into table application_roles + for role in roles { + sqlx::query!( + " + INSERT INTO application_roles (application_id, campaign_role_id, preference) + VALUES ($1, $2, $3) + ", + id, + role_applied.campaign_role_id, + role_applied.preference + ) + .execute(transaction.deref_mut()) + .await?; + } + } } From 299ee9bf34fa1060de75b45be534870905d9fe95 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 00:49:13 +1100 Subject: [PATCH 07/20] lock out user from application changes after submission --- .../20240406031915_create_applications.sql | 1 + backend/server/src/handler/application.rs | 12 +++- backend/server/src/models/answer.rs | 36 ++++++++++- backend/server/src/models/application.rs | 61 +++++++++++++------ 4 files changed, 90 insertions(+), 20 deletions(-) diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index e767edde..cca4c04f 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -8,6 +8,7 @@ CREATE TABLE applications ( private_status application_status NOT NULL DEFAULT 'Pending', created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + submitted BOOLEAN NOT NULL DEFAULT false, CONSTRAINT FK_applications_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) diff --git a/backend/server/src/handler/application.rs b/backend/server/src/handler/application.rs index 3c84b3cd..7c9cdca5 100644 --- a/backend/server/src/handler/application.rs +++ b/backend/server/src/handler/application.rs @@ -53,10 +53,20 @@ impl ApplicationHandler { _user: ApplicationOwner, Path(application_id): Path, mut transaction: DBTransaction<'_>, - Json(data): Json + Json(data): Json, ) -> Result { Application::update_roles(application_id, data.roles, &mut transaction.tx).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully updated application roles")) } + + pub async fn submit( + _user: ApplicationOwner, + Path(application_id): Path, + mut transaction: DBTransaction<'_>, + ) -> Result { + Application::submit(application_id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully submitted application")) + } } diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index 3b5d2fb1..93ea3f0d 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -69,6 +69,16 @@ impl Answer { ) -> Result { answer_data.validate()?; + // Can only answer for applications that haven't been submitted + let _ = sqlx::query!( + " + SELECT id FROM applications WHERE id = $1 AND submitted = false + ", + application_id + ) + .fetch_one(transaction.deref_mut()) + .await?; + let id = snowflake_generator.generate(); sqlx::query!( @@ -299,6 +309,16 @@ impl Answer { .fetch_one(transaction.deref_mut()) .await?; + // Can only answer for applications that haven't been submitted + let _ = sqlx::query!( + " + SELECT id FROM applications WHERE id = $1 AND submitted = false + ", + answer.application_id + ) + .fetch_one(transaction.deref_mut()) + .await?; + let old_data = AnswerData::from_question_type(&answer.question_type); old_data.delete_from_db(id, transaction).await?; @@ -319,10 +339,24 @@ impl Answer { id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - sqlx::query!("DELETE FROM answers WHERE id = $1 RETURNING id", id) + let answer = sqlx::query!("SELECT application_id FROM answers WHERE id = $1", id) .fetch_one(transaction.deref_mut()) .await?; + // Can only answer for applications that haven't been submitted + let _ = sqlx::query!( + " + SELECT id FROM applications WHERE id = $1 AND submitted = false + ", + answer.application_id + ) + .fetch_one(transaction.deref_mut()) + .await?; + + sqlx::query!("DELETE FROM answers WHERE id = $1", id) + .execute(transaction.deref_mut()) + .await?; + Ok(()) } } diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index fd3d3df8..c6b403d0 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -70,7 +70,7 @@ pub struct ApplicationAppliedRoleDetails { #[derive(Deserialize)] pub struct ApplicationRoleUpdate { - pub roles: Vec + pub roles: Vec, } #[derive(Deserialize, Serialize, sqlx::Type, Clone, Debug)] @@ -123,7 +123,7 @@ impl Application { } /* - Get Application given an application id + Get Application given an application id. Used by application viewers */ pub async fn get( id: i64, @@ -138,7 +138,7 @@ impl Application { u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year FROM applications a LEFT JOIN users u ON u.id = a.user_id - WHERE a.id = $1 + WHERE a.id = $1 AND a.submitted = true ", id ) @@ -180,7 +180,7 @@ impl Application { } /* - Get All applications that apply for a given role + Get All applications that apply for a given role. Used by application viewers */ pub async fn get_from_role_id( role_id: i64, @@ -195,7 +195,7 @@ impl Application { u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year FROM applications a LEFT JOIN users u ON u.id = a.user_id LEFT JOIN application_roles ar on ar.application_id = a.id - WHERE ar.id = $1 + WHERE ar.id = $1 AND a.submitted = true ", role_id ) @@ -244,7 +244,7 @@ impl Application { } /* - Get All applications that apply for a given campaign + Get All applications that apply for a given campaign. Used by application viewers */ pub async fn get_from_campaign_id( campaign_id: i64, @@ -259,7 +259,7 @@ impl Application { u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year FROM applications a LEFT JOIN users u ON u.id = a.user_id - WHERE a.campaign_id = $1 + WHERE a.campaign_id = $1 AND a.submitted = true ", campaign_id ) @@ -308,7 +308,7 @@ impl Application { } /* - Get All applications that are made by a given user + Get All applications that are made by a given user. Used by user */ pub async fn get_from_user_id( user_id: i64, @@ -414,20 +414,25 @@ impl Application { pub async fn update_roles( id: i64, - roles: Vec, + roles: Vec, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - // There can be 0 roles, so we cannot use the `RETURNING id` method, - // as there could be 0 roles. If application_id is wrong, then - // the next query will error, preventing changes to DB. + // Users can only update applications as long as they have not submitted + let _ = sqlx::query!( + "SELECT id FROM applications WHERE id = $1 AND submitted = false", + id + ) + .fetch_one(transaction.deref_mut()) + .await?; + sqlx::query!( " DELETE FROM application_roles WHERE application_id = $1 ", id ) - .execute(transaction.deref_mut()) - .await?; + .execute(transaction.deref_mut()) + .await?; // Insert into table application_roles for role in roles { @@ -437,11 +442,31 @@ impl Application { VALUES ($1, $2, $3) ", id, - role_applied.campaign_role_id, - role_applied.preference + role.campaign_role_id, + role.preference ) - .execute(transaction.deref_mut()) - .await?; + .execute(transaction.deref_mut()) + .await?; } + + Ok(()) + } + + pub async fn submit( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + // Can only submit once + let _ = sqlx::query!( + " + UPDATE applications SET submitted = true + WHERE id = $1 AND submitted = false RETURNING id + ", + id + ) + .fetch_one(transaction.deref_mut()) + .await?; + + Ok(()) } } From 43e47cd81e21d53d566d5afd13a2a0a576ef5df3 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 01:06:30 +1100 Subject: [PATCH 08/20] lock application after submission and campaign close date --- backend/server/src/handler/answer.rs | 4 ++ backend/server/src/handler/application.rs | 3 +- backend/server/src/models/answer.rs | 36 +----------- backend/server/src/models/application.rs | 72 +++++++++++++++++++---- backend/server/src/models/error.rs | 4 ++ backend/server/src/service/answer.rs | 25 ++++++++ backend/server/src/service/application.rs | 24 ++++++++ 7 files changed, 121 insertions(+), 47 deletions(-) diff --git a/backend/server/src/handler/answer.rs b/backend/server/src/handler/answer.rs index 065be1da..102e586b 100644 --- a/backend/server/src/handler/answer.rs +++ b/backend/server/src/handler/answer.rs @@ -7,6 +7,7 @@ use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; use serde_json::json; +use crate::models::application::{OpenApplicationByAnswerId, OpenApplicationByApplicationId}; pub struct AnswerHandler; @@ -15,6 +16,7 @@ impl AnswerHandler { State(state): State, Path(application_id): Path, _user: ApplicationOwner, + _: OpenApplicationByApplicationId, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { @@ -63,6 +65,7 @@ impl AnswerHandler { pub async fn update( Path(answer_id): Path, _owner: AnswerOwner, + _: OpenApplicationByAnswerId, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { @@ -76,6 +79,7 @@ impl AnswerHandler { pub async fn delete( Path(answer_id): Path, _owner: AnswerOwner, + _: OpenApplicationByAnswerId, mut transaction: DBTransaction<'_>, ) -> Result { Answer::delete(answer_id, &mut transaction.tx).await?; diff --git a/backend/server/src/handler/application.rs b/backend/server/src/handler/application.rs index 7c9cdca5..00afd7cf 100644 --- a/backend/server/src/handler/application.rs +++ b/backend/server/src/handler/application.rs @@ -1,5 +1,5 @@ use crate::models::app::AppState; -use crate::models::application::{Application, ApplicationRoleUpdate, ApplicationStatus}; +use crate::models::application::{Application, ApplicationRoleUpdate, ApplicationStatus, OpenApplicationByApplicationId}; use crate::models::auth::{ApplicationAdmin, ApplicationOwner, AuthUser}; use crate::models::error::ChaosError; use crate::models::transaction::DBTransaction; @@ -62,6 +62,7 @@ impl ApplicationHandler { pub async fn submit( _user: ApplicationOwner, + _: OpenApplicationByApplicationId, Path(application_id): Path, mut transaction: DBTransaction<'_>, ) -> Result { diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index 93ea3f0d..793ef0db 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -69,16 +69,6 @@ impl Answer { ) -> Result { answer_data.validate()?; - // Can only answer for applications that haven't been submitted - let _ = sqlx::query!( - " - SELECT id FROM applications WHERE id = $1 AND submitted = false - ", - application_id - ) - .fetch_one(transaction.deref_mut()) - .await?; - let id = snowflake_generator.generate(); sqlx::query!( @@ -309,16 +299,6 @@ impl Answer { .fetch_one(transaction.deref_mut()) .await?; - // Can only answer for applications that haven't been submitted - let _ = sqlx::query!( - " - SELECT id FROM applications WHERE id = $1 AND submitted = false - ", - answer.application_id - ) - .fetch_one(transaction.deref_mut()) - .await?; - let old_data = AnswerData::from_question_type(&answer.question_type); old_data.delete_from_db(id, transaction).await?; @@ -339,24 +319,10 @@ impl Answer { id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - let answer = sqlx::query!("SELECT application_id FROM answers WHERE id = $1", id) + let _ = sqlx::query!("DELETE FROM answers WHERE id = $1 RETURNING id", id) .fetch_one(transaction.deref_mut()) .await?; - // Can only answer for applications that haven't been submitted - let _ = sqlx::query!( - " - SELECT id FROM applications WHERE id = $1 AND submitted = false - ", - answer.application_id - ) - .fetch_one(transaction.deref_mut()) - .await?; - - sqlx::query!("DELETE FROM answers WHERE id = $1", id) - .execute(transaction.deref_mut()) - .await?; - Ok(()) } } diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index c6b403d0..66a3e94e 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use crate::models::error::ChaosError; use crate::models::user::UserDetails; use chrono::{DateTime, Utc}; @@ -5,6 +6,12 @@ use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{FromRow, Pool, Postgres, Transaction}; use std::ops::DerefMut; +use axum::{async_trait, RequestPartsExt}; +use axum::extract::{FromRef, FromRequestParts, Path}; +use axum::http::request::Parts; +use crate::models::app::AppState; +use crate::service::answer::assert_answer_application_is_open; +use crate::service::application::{assert_application_is_open}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Application { @@ -417,14 +424,6 @@ impl Application { roles: Vec, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - // Users can only update applications as long as they have not submitted - let _ = sqlx::query!( - "SELECT id FROM applications WHERE id = $1 AND submitted = false", - id - ) - .fetch_one(transaction.deref_mut()) - .await?; - sqlx::query!( " DELETE FROM application_roles WHERE application_id = $1 @@ -456,11 +455,9 @@ impl Application { id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - // Can only submit once let _ = sqlx::query!( " - UPDATE applications SET submitted = true - WHERE id = $1 AND submitted = false RETURNING id + UPDATE applications SET submitted = true WHERE id = $1 RETURNING id ", id ) @@ -470,3 +467,56 @@ impl Application { Ok(()) } } + + +pub struct OpenApplicationByApplicationId; + +#[async_trait] +impl FromRequestParts for OpenApplicationByApplicationId +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 application_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_application_is_open(application_id, &app_state.db).await?; + + Ok(OpenApplicationByApplicationId) + } +} + +pub struct OpenApplicationByAnswerId; + +#[async_trait] +impl FromRequestParts for OpenApplicationByAnswerId +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 answer_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_answer_application_is_open(answer_id, &app_state.db).await?; + + Ok(OpenApplicationByAnswerId) + } +} \ No newline at end of file diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 734ca8ba..58ca6b11 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -19,6 +19,9 @@ pub enum ChaosError { #[error("Bad request")] BadRequest, + #[error("Application closed")] + ApplicationClosed, + #[error("SQLx error")] DatabaseError(#[from] sqlx::Error), @@ -66,6 +69,7 @@ impl IntoResponse for ChaosError { (StatusCode::FORBIDDEN, "Forbidden operation").into_response() } ChaosError::BadRequest => (StatusCode::BAD_REQUEST, "Bad request").into_response(), + ChaosError::ApplicationClosed => (StatusCode::BAD_REQUEST, "Application closed").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/answer.rs b/backend/server/src/service/answer.rs index 682656a0..a0f8ca5b 100644 --- a/backend/server/src/service/answer.rs +++ b/backend/server/src/service/answer.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -30,3 +31,27 @@ pub async fn user_is_answer_owner( Ok(()) } + +pub async fn assert_answer_application_is_open( + answer_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let application = sqlx::query!( + " + SELECT app.submitted, c.ends_at FROM answers ans + JOIN applications app ON app.id = ans.application_id + JOIN campaigns c on c.id = app.campaign_id + WHERE ans.id = $1 + ", + answer_id + ) + .fetch_one(pool) + .await?; + + if application.submitted || application.ends_at <= time { + return Err(ChaosError::ApplicationClosed) + } + + Ok(()) +} \ No newline at end of file diff --git a/backend/server/src/service/application.rs b/backend/server/src/service/application.rs index dcdf755b..9198f167 100644 --- a/backend/server/src/service/application.rs +++ b/backend/server/src/service/application.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -60,3 +61,26 @@ pub async fn user_is_application_owner( Ok(()) } + +pub async fn assert_application_is_open( + application_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let application = sqlx::query!( + " + SELECT submitted, c.ends_at FROM applications a + JOIN campaigns c on c.id = a.campaign_id + WHERE a.id = $1 + ", + application_id + ) + .fetch_one(pool) + .await?; + + if application.submitted || application.ends_at <= time { + return Err(ChaosError::ApplicationClosed) + } + + Ok(()) +} From 80c1abe5f4e378346e966fc70c25af7b0bbe0802 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 01:13:05 +1100 Subject: [PATCH 09/20] fix uses of `LEFT JOIN` when `JOIN` was needed --- backend/server/src/models/application.rs | 16 ++++++++-------- backend/server/src/models/organisation.rs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index 66a3e94e..6099646f 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -144,7 +144,7 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id + FROM applications a JOIN users u ON u.id = a.user_id WHERE a.id = $1 AND a.submitted = true ", id @@ -158,7 +158,7 @@ impl Application { SELECT application_roles.campaign_role_id, application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", @@ -201,7 +201,7 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id LEFT JOIN application_roles ar on ar.application_id = a.id + FROM applications a JOIN users u ON u.id = a.user_id JOIN application_roles ar on ar.application_id = a.id WHERE ar.id = $1 AND a.submitted = true ", role_id @@ -217,7 +217,7 @@ impl Application { SELECT application_roles.campaign_role_id, application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", @@ -265,7 +265,7 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id + FROM applications a JOIN users u ON u.id = a.user_id WHERE a.campaign_id = $1 AND a.submitted = true ", campaign_id @@ -281,7 +281,7 @@ impl Application { SELECT application_roles.campaign_role_id, application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", @@ -329,7 +329,7 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id + FROM applications a JOIN users u ON u.id = a.user_id WHERE a.user_id = $1 ", user_id @@ -345,7 +345,7 @@ impl Application { SELECT application_roles.campaign_role_id, application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 8942ad2f..ae3e5229 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -194,7 +194,7 @@ impl Organisation { 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 + JOIN users on users.id = organisation_members.user_id WHERE organisation_members.organisation_id = $1 AND organisation_members.role = $2 ", organisation_id, @@ -216,7 +216,7 @@ impl Organisation { 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 + JOIN users on users.id = organisation_members.user_id WHERE organisation_members.organisation_id = $1 ", organisation_id From efe6c00a901d832a7767a6b517fa71d57da7edcb Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 01:16:59 +1100 Subject: [PATCH 10/20] make unsubmitted application viewable after campaign end --- backend/server/src/models/application.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index 6099646f..06d99af5 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -144,8 +144,10 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a JOIN users u ON u.id = a.user_id - WHERE a.id = $1 AND a.submitted = true + FROM applications a + JOIN users u ON u.id = a.user_id + JOIN campaigns c ON c.id = a.campaign_id + WHERE a.id = $1 AND (a.submitted = true OR c.ends_at <= CURRENT_TIMESTAMP) ", id ) @@ -201,8 +203,11 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a JOIN users u ON u.id = a.user_id JOIN application_roles ar on ar.application_id = a.id - WHERE ar.id = $1 AND a.submitted = true + FROM applications a + JOIN users u ON u.id = a.user_id + JOIN application_roles ar on ar.application_id = a.id + JOIN campaigns c on c.id = a.campaign_id + WHERE ar.id = $1 AND (a.submitted = true OR c.ends_at <= CURRENT_TIMESTAMP) ", role_id ) @@ -265,8 +270,10 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a JOIN users u ON u.id = a.user_id - WHERE a.campaign_id = $1 AND a.submitted = true + FROM applications a + JOIN users u ON u.id = a.user_id + JOIN campaigns c ON c.id = a.campaign_id + WHERE a.campaign_id = $1 AND (a.submitted = true OR c.ends_at <= CURRENT_TIMESTAMP) ", campaign_id ) From 02b705b22a6f83b983f9a441900ff2c35576d350 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 01:19:02 +1100 Subject: [PATCH 11/20] only submitted applications are viewable by reviewers --- backend/server/src/models/application.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index 06d99af5..0dcafab1 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -147,7 +147,7 @@ impl Application { FROM applications a JOIN users u ON u.id = a.user_id JOIN campaigns c ON c.id = a.campaign_id - WHERE a.id = $1 AND (a.submitted = true OR c.ends_at <= CURRENT_TIMESTAMP) + WHERE a.id = $1 AND a.submitted = true ", id ) @@ -207,7 +207,7 @@ impl Application { JOIN users u ON u.id = a.user_id JOIN application_roles ar on ar.application_id = a.id JOIN campaigns c on c.id = a.campaign_id - WHERE ar.id = $1 AND (a.submitted = true OR c.ends_at <= CURRENT_TIMESTAMP) + WHERE ar.id = $1 AND a.submitted = true ", role_id ) @@ -273,7 +273,7 @@ impl Application { FROM applications a JOIN users u ON u.id = a.user_id JOIN campaigns c ON c.id = a.campaign_id - WHERE a.campaign_id = $1 AND (a.submitted = true OR c.ends_at <= CURRENT_TIMESTAMP) + WHERE a.campaign_id = $1 AND a.submitted = true ", campaign_id ) From 6fcb06ad7f665394526d21ebee6fa15d9c37231a Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 01:55:02 +1100 Subject: [PATCH 12/20] added `Answer`-related schemas to api.yaml --- backend/api.yaml | 64 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/backend/api.yaml b/backend/api.yaml index 729d4030..6e6433c5 100644 --- a/backend/api.yaml +++ b/backend/api.yaml @@ -1508,7 +1508,7 @@ paths: schema: properties: application: - type: + type: $ref: '#components/schemas/ApplicationDetails' '401': description: Not logged in. @@ -1638,6 +1638,68 @@ paths: components: schemas: + Answer: + type: object + properties: + id: + type: integer + format: int64 + example: 6996987893965227483 + question_id: + type: integer + format: int64 + example: 6996987893965227483 + answer_type: + type: string + enum: + - ShortAnswer + - MultiChoice + - MultiSelect + - DropDown + - Ranking + data: + oneOf: + - type: string + example: I am passionate about events + - type: integer + example: 6996987893965227483 + - type: array + format: int64 + example: [6996987893965227483, 69969829832652228374, 6996987893965228374] + created_at: + type: string + example: 2024-03-15T18:25:43.511Z + updated_at: + type: string + example: 2024-03-15T18:25:43.511Z + + NewAnswer: + type: object + properties: + question_id: + type: integer + format: int64 + example: 6996987893965262849 + answer_type: + type: string + enum: + - ShortAnswer + - MultiChoice + - MultiSelect + - DropDown + - Ranking + data: + oneOf: + - type: string + example: I am passionate about events + - type: integer + example: 6996987893965227483 + - type: array + format: int64 + example: [6996987893965227483, 69969829832652228374, 6996987893965228374] + + + ApplicationStatus: type: string enum: From c08e4b4cbef733465a22e9f3f1c1131ed8010cbd Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 12:56:20 +1100 Subject: [PATCH 13/20] register new `ApplicationHandler` endpoints in app --- backend/server/src/models/app.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index f4114db8..fd42647d 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -248,6 +248,14 @@ pub async fn app() -> Result { "/api/v1/application/:application_id/answers/role/:role_id", get(AnswerHandler::get_all_by_application_and_role), ) + .route( + "/api/v1/application/:application_id/roles", + patch(ApplicationHandler::update_roles) + ) + .route( + "/api/v1/application/:application_id/submit", + post(ApplicationHandler::submit) + ) .route( "/api/v1/answer/:answer_id", patch(AnswerHandler::update).delete(AnswerHandler::delete), From d2d402a0bbbf76a1cd4ed6ab028e303e3b490a28 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 13:33:10 +1100 Subject: [PATCH 14/20] completed up to /organisation/slug_check --- backend/api.yaml | 546 ++++++++++++++++++++++++----------------------- 1 file changed, 275 insertions(+), 271 deletions(-) diff --git a/backend/api.yaml b/backend/api.yaml index 6e6433c5..0e60bbd3 100644 --- a/backend/api.yaml +++ b/backend/api.yaml @@ -9,158 +9,84 @@ servers: description: Local server paths: - /auth/logout: - post: - operationId: logout - description: Invalidates current token. + /: + get: + operationId: getRoot + description: Root of API tags: - - Auth + - Miscellaneous responses: - '200': + "200": description: OK content: - application/json: + text/plain: schema: - properties: - messages: - type: string - example: Successfully logged out. - /user: + type: string + example: Join DevSoc! https://devsoc.app/ + + /auth/callback/google: get: - operationId: getLoggedInUser - description: Returns info about currently logged in user. + operationId: googleCallback + description: Google OAuth callback tags: - - User + - Auth + parameters: + - name: code + in: query + description: Google OAuth code + required: true + schema: + type: string responses: - '200': - description: OK - content: - application/json: - schema: - properties: - id: - type: integer - format: int64 - example: 1541815603606036480 - email: - type: string - example: me@example.com - zid: - type: string - example: z5555555 - name: - type: string - example: Clancy Lion - degree_name: - type: string - example: Computer Science - degree_starting_year: - type: integer - example: 2024 - role: - type: string - example: User - '401': - description: Not logged in. + "200": + description: Ok content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - delete: - operationId: deleteUserById - description: Deletes currently logged in user. + /auth/logout: + post: + operationId: logout + description: Invalidates current token tags: - - User + - Auth responses: - '200': - description: OK + "200": + description: Ok content: application/json: schema: properties: message: type: string - example: Successfully deleted user. - '403': - description: User is only admin of an organisation. - content: - application/json: - schema: - properties: - error: - type: string - example: "Cannot delete sole admin of an organisation." - '401': - description: Not logged. + example: Successfully logged out + "401": + description: Not logged in content: application/json: schema: - properties: - error: - type: string - example: Not logged in. - /user/{id}: + $ref: "#/components/schemas/NotLoggedIn" + + /user: get: - operationId: getUserById - parameters: - - name: id - in: path - description: User ID - required: true - schema: - type: integer - format: int64 - description: Returns info about specified user. + operationId: getLoggedInUser + description: Returns info about currently logged in user tags: - User responses: - '200': + "200": description: OK content: application/json: schema: - properties: - id: - type: integer - format: int64 - example: 1541815603606036480 - email: - type: string - example: me@example.com - zid: - type: string - example: z5555555 - name: - type: string - example: Clancy Lion - pronouns: - type: string - example: They/Them - gender: - type: string - example: Male - degree_name: - type: string - example: Computer Science - degree_starting_year: - type: integer - example: 2024 - '403': - description: Requested user has not applied to one of authorized user's campaign. + $ref: "#/components/schemas/User" + "401": + description: Not logged in content: application/json: schema: - properties: - error: - type: string - example: "Insufficient permissions" + $ref: "#/components/schemas/NotLoggedIn" /user/name: patch: operationId: updateUserName - description: Updates currently logged in user's name. + description: Updates currently logged in user's name tags: - User requestBody: @@ -173,7 +99,7 @@ paths: type: string example: "Clancy Tiger" responses: - '200': + "200": description: OK content: application/json: @@ -181,21 +107,17 @@ paths: properties: message: type: string - example: Successfully updated name. - '401': + example: Successfully updated name + "401": description: Not logged in. content: application/json: schema: - properties: - error: - type: string - example: Not logged in. - + $ref: "#/components/schemas/NotLoggedIn" /user/pronouns: patch: operationId: updateUserPronouns - description: Updates currently logged in user's pronouns. + description: Updates currently logged in user's pronouns tags: - User requestBody: @@ -204,11 +126,11 @@ paths: application/json: schema: properties: - zid: + pronouns: type: string - example: "z5123456" + example: They/Them responses: - '200': + "200": description: OK content: application/json: @@ -216,20 +138,17 @@ paths: properties: message: type: string - example: Successfully updated pronouns. - '401': + example: Successfully updated pronouns + "401": description: Not logged in. content: application/json: schema: - properties: - error: - type: string - example: Not logged in. + $ref: "#/components/schemas/NotLoggedIn" /user/gender: patch: operationId: updateUserGender - description: Updates currently logged in user's gender. + description: Updates currently logged in user's gender tags: - User requestBody: @@ -238,11 +157,11 @@ paths: application/json: schema: properties: - zid: + gender: type: string - example: "z5123456" + example: Female responses: - '200': + "200": description: OK content: application/json: @@ -250,22 +169,17 @@ paths: properties: message: type: string - example: Successfully updated gender. - '401': + example: Successfully updated gender + "401": description: Not logged in. content: application/json: schema: - properties: - error: - type: string - example: Not logged in. - - + $ref: "#/components/schemas/NotLoggedIn" /user/zid: patch: operationId: updateUserZid - description: Updates currently logged in user's zID. + description: Updates currently logged in user's zID tags: - User requestBody: @@ -276,9 +190,9 @@ paths: properties: zid: type: string - example: "z5123456" + example: z5123456 responses: - '200': + "200": description: OK content: application/json: @@ -286,20 +200,17 @@ paths: properties: message: type: string - example: Successfully updated zID. - '401': + example: Successfully updated zID + "401": description: Not logged in. content: application/json: schema: - properties: - error: - type: string - example: Not logged in. + $ref: "#/components/schemas/NotLoggedIn" /user/degree: patch: operationId: updateUserDegree - description: Updates currently logged in user's degree. + description: Updates currently logged in user's degree tags: - User requestBody: @@ -310,12 +221,12 @@ paths: properties: degree_name: type: string - example: "Electrical Engineering" + example: Electrical Engineering degree_starting_year: type: integer example: 2024 responses: - '200': + "200": description: OK content: application/json: @@ -323,25 +234,21 @@ paths: properties: message: type: string - example: Successfully updated email. - '401': + example: Successfully updated email + "401": description: Not logged in. content: application/json: schema: - properties: - error: - type: string - example: Not logged in. - + $ref: "#/components/schemas/NotLoggedIn" /user/applications: get: operationId: getUserApplications - description: Returns info about applications made by currently logged in user. + description: Returns info about applications made by currently logged in user tags: - User responses: - '200': + "200": description: OK content: application/json: @@ -350,23 +257,18 @@ paths: campaigns: type: array items: - $ref: '#/components/schemas/ApplicationDetails' - '401': + $ref: "#/components/schemas/ApplicationDetails" + "401": description: Not logged in. content: application/json: schema: - properties: - error: - type: string - example: Not logged in. - - + $ref: "#/components/schemas/NotLoggedIn" /organisation: post: operationId: createOrganisation - description: Creates a new organisation. + description: Create a new organisation tags: - Organisation requestBody: @@ -375,16 +277,20 @@ paths: application/json: schema: properties: + slug: + type: string + description: ASCII string for URL like https://chaos.csesoc.app/s/unsw-devsoc + example: unsw-devsoc name: type: string - example: "UNSW Software Development Society" + example: UNSW Software Development Society admin: type: integer format: int64 example: 1541815603606036480 description: User ID of admin responses: - '200': + "200": description: OK content: application/json: @@ -392,16 +298,44 @@ paths: properties: message: type: string - example: Successfully created organisation. - '403': - description: User is not a super user. + example: Successfully created organisation + "403": + description: User is not a SuperUser + content: + application/json: + schema: + $ref: "#/components/schemas/Unauthorized" + /organisation/slug_check: + post: + operationId: checkOrganisationSlugAvailability + description: Check if slug is available + tags: + - Organisation + requestBody: + required: true + content: + application/json: + schema: + properties: + slug: + type: string + example: unsw-devsoc + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Unauthorized + example: Organisation slug is available + "400": + description: Bad request - slug is in use or not ASCII + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequest" /organisation/{id}: get: operationId: getOrganisationById @@ -417,7 +351,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -436,7 +370,7 @@ paths: created_at: type: string example: 2024-02-10T18:25:43.511Z - '401': + "401": description: Not logged in. content: application/json: @@ -459,7 +393,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -468,7 +402,7 @@ paths: message: type: string example: Successfully deleted organisation. - '401': + "401": description: Not logged in. content: application/json: @@ -477,7 +411,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a super user. content: application/json: @@ -501,7 +435,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -510,8 +444,8 @@ paths: campaigns: type: array items: - $ref: '#/components/schemas/OrganisationCampaign' - '401': + $ref: "#/components/schemas/OrganisationCampaign" + "401": description: Not logged in. content: application/json: @@ -536,7 +470,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -546,7 +480,7 @@ paths: type: string description: Presigned S3 url to upload file. example: https://www.youtube.com/watch?v=dQw4w9WgXcQ - '401': + "401": description: Not logged in. content: application/json: @@ -555,7 +489,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -580,7 +514,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -601,7 +535,7 @@ paths: role: type: string example: Admin - '403': + "403": description: User is not an organisation admin or member. content: application/json: @@ -632,12 +566,17 @@ paths: items: type: integer format: int64 - example: [1541815603606036480, 1541815603606036827, 1541815287306036429] + example: + [ + 1541815603606036480, + 1541815603606036827, + 1541815287306036429, + ] description: Specifies members for specified organistion. tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -646,7 +585,7 @@ paths: message: type: string example: Successfully updated members. - '401': + "401": description: Not logged in. content: application/json: @@ -655,7 +594,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -687,7 +626,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -696,7 +635,7 @@ paths: message: type: string example: Successfully updated members. - '401': + "401": description: Not logged in. content: application/json: @@ -705,7 +644,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -730,7 +669,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -751,7 +690,7 @@ paths: role: type: string example: Admin - '403': + "403": description: User is not a SuperUser. content: application/json: @@ -782,12 +721,17 @@ paths: items: type: integer format: int64 - example: [1541815603606036480, 1541815603606036827, 1541815287306036429] + example: + [ + 1541815603606036480, + 1541815603606036827, + 1541815287306036429, + ] description: Specifies Admins for specified organistion. tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -796,7 +740,7 @@ paths: message: type: string example: Successfully updated members. - '401': + "401": description: Not logged in. content: application/json: @@ -805,7 +749,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a SuperUser. content: application/json: @@ -837,7 +781,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -846,7 +790,7 @@ paths: message: type: string example: Successfully deleted Admin. - '401': + "401": description: Not logged in. content: application/json: @@ -855,7 +799,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a SuperUser. content: application/json: @@ -898,7 +842,7 @@ paths: type: string example: 2024-04-15T18:25:43.511Z responses: - '200': + "200": description: OK content: application/json: @@ -907,7 +851,7 @@ paths: message: type: string example: Successfully created campaign. - '403': + "403": description: User is not an admin of specified organisation. content: application/json: @@ -923,7 +867,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -975,7 +919,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1039,7 +983,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1048,7 +992,7 @@ paths: message: type: string example: Successfully updated campaign. - '401': + "401": description: Not logged in. content: application/json: @@ -1057,7 +1001,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -1080,7 +1024,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1089,7 +1033,7 @@ paths: message: type: string example: Successfully deleted campaign. - '403': + "403": description: User is not an admin of campaign's organisation. content: application/json: @@ -1113,7 +1057,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1123,7 +1067,7 @@ paths: type: string description: Presigned S3 url to upload file. example: https://www.youtube.com/watch?v=dQw4w9WgXcQ - '401': + "401": description: Not logged in. content: application/json: @@ -1132,7 +1076,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -1180,7 +1124,7 @@ paths: description: Whether this role has been finalised (e.g. max avaliable number) example: False responses: - '200': + "200": description: OK content: application/json: @@ -1189,7 +1133,7 @@ paths: message: type: string example: Successfully created organisation. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1214,7 +1158,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1223,8 +1167,8 @@ paths: campaigns: type: array items: - $ref: '#components/schemas/RoleDetails' - '401': + $ref: "#components/schemas/RoleDetails" + "401": description: Not logged in. content: application/json: @@ -1233,7 +1177,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1258,7 +1202,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1267,8 +1211,8 @@ paths: applications: type: array items: - $ref: '#components/schemas/ApplicationDetails' - '401': + $ref: "#components/schemas/ApplicationDetails" + "401": description: Not logged in. content: application/json: @@ -1277,7 +1221,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1287,7 +1231,6 @@ paths: type: string example: Unauthorized - /role/{id}: get: operationId: getRoleById @@ -1303,7 +1246,7 @@ paths: tags: - Role responses: - '200': + "200": description: OK content: application/json: @@ -1325,7 +1268,7 @@ paths: type: boolean description: Whether this role has been finalised (e.g. max avaliable number) example: False - '401': + "401": description: Not logged in. content: application/json: @@ -1372,7 +1315,7 @@ paths: description: Whether this role has been finalised (e.g. max avaliable number) example: true responses: - '200': + "200": description: OK content: application/json: @@ -1381,7 +1324,7 @@ paths: message: type: string example: Successfully update organisation. - '401': + "401": description: Not logged in. content: application/json: @@ -1390,7 +1333,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1414,7 +1357,7 @@ paths: tags: - Role responses: - '200': + "200": description: OK content: application/json: @@ -1423,7 +1366,7 @@ paths: message: type: string example: Successfully deleted role. - '401': + "401": description: Not logged in. content: application/json: @@ -1432,7 +1375,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an admin of role's Campaign. content: application/json: @@ -1457,7 +1400,7 @@ paths: tags: - Role responses: - '200': + "200": description: OK content: application/json: @@ -1466,8 +1409,8 @@ paths: applications: type: array items: - $ref: '#components/schemas/ApplicationDetails' - '401': + $ref: "#components/schemas/ApplicationDetails" + "401": description: Not logged in. content: application/json: @@ -1476,7 +1419,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1501,7 +1444,7 @@ paths: tags: - Application responses: - '200': + "200": description: OK content: application/json: @@ -1509,8 +1452,8 @@ paths: properties: application: type: - $ref: '#components/schemas/ApplicationDetails' - '401': + $ref: "#components/schemas/ApplicationDetails" + "401": description: Not logged in. content: application/json: @@ -1519,7 +1462,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1529,7 +1472,6 @@ paths: type: string example: Unauthorized - /application/{id}/private: put: operationId: updateApplicationPrivateStatus @@ -1549,13 +1491,13 @@ paths: properties: data: type: - $ref: '#components/schemas/ApplicationStatus' + $ref: "#components/schemas/ApplicationStatus" description: Change Private Status of a specific Application tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -1564,7 +1506,7 @@ paths: message: type: string example: Successfully updated Application Private Status. - '401': + "401": description: Not logged in. content: application/json: @@ -1573,7 +1515,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1602,13 +1544,13 @@ paths: properties: data: type: - $ref: '#components/schemas/ApplicationStatus' + $ref: "#components/schemas/ApplicationStatus" description: Change Status of a specific Application tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -1617,7 +1559,7 @@ paths: message: type: string example: Successfully updated Application Status. - '401': + "401": description: Not logged in. content: application/json: @@ -1626,7 +1568,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1638,6 +1580,27 @@ paths: components: schemas: + NotLoggedIn: + type: object + properties: + message: + type: string + example: Not logged in + + Unauthorized: + type: object + properties: + message: + type: string + example: Unauthorized + + BadRequest: + type: object + properties: + message: + type: string + example: Bad request + Answer: type: object properties: @@ -1665,7 +1628,8 @@ components: example: 6996987893965227483 - type: array format: int64 - example: [6996987893965227483, 69969829832652228374, 6996987893965228374] + example: + [6996987893965227483, 69969829832652228374, 6996987893965228374] created_at: type: string example: 2024-03-15T18:25:43.511Z @@ -1696,9 +1660,8 @@ components: example: 6996987893965227483 - type: array format: int64 - example: [6996987893965227483, 69969829832652228374, 6996987893965228374] - - + example: + [6996987893965227483, 69969829832652228374, 6996987893965228374] ApplicationStatus: type: string @@ -1758,6 +1721,43 @@ components: description: Whether this role has been finalised (e.g. max avaliable number) example: False + User: + type: object + properties: + id: + type: integer + format: int64 + example: 1541815603606036480 + email: + type: string + example: me@example.com + zid: + type: string + example: z5555555 + nullable: true + name: + type: string + example: Clancy Lion + pronouns: + type: string + example: He/Him + gender: + type: string + example: Male + degree_name: + type: string + example: Computer Science + nullable: true + degree_starting_year: + type: integer + example: 2024 + nullable: true + role: + type: string + enum: + - User + - SuperUser + UserDetails: type: object properties: @@ -1799,15 +1799,15 @@ components: format: int64 example: 5141815603606036480 user: - $ref: '#/components/schemas/UserDetails' + $ref: "#/components/schemas/UserDetails" status: - $ref: '#/components/schemas/ApplicationStatus' + $ref: "#/components/schemas/ApplicationStatus" private_status: - $ref: '#/components/schemas/ApplicationStatus' + $ref: "#/components/schemas/ApplicationStatus" applied_roles: type: array items: - $ref: '#/components/schemas/ApplicationAppliedRoleDetails' + $ref: "#/components/schemas/ApplicationAppliedRoleDetails" ApplicationAppliedRoleDetails: type: object @@ -1818,4 +1818,8 @@ components: example: 1541815603606036480 role_name: type: String - example: UI/UX subcom \ No newline at end of file + example: Sponsorships + preference: + type: integer + format: int32 + example: 1 From b2b742644892ebe2f87b90ec5ce60fefefd600f4 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 19:03:36 +1100 Subject: [PATCH 15/20] `NewCampaign` model for campaign create request body --- backend/server/src/handler/organisation.rs | 4 ++-- backend/server/src/models/campaign.rs | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 313fabfd..a317d1c5 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,7 +1,7 @@ use crate::models::app::AppState; use crate::models::auth::SuperUser; use crate::models::auth::{AuthUser, OrganisationAdmin}; -use crate::models::campaign::Campaign; +use crate::models::campaign::{Campaign, NewCampaign}; use crate::models::email_template::EmailTemplate; use crate::models::error::ChaosError; use crate::models::organisation::{ @@ -166,7 +166,7 @@ impl OrganisationHandler { Path(id): Path, State(state): State, _admin: OrganisationAdmin, - Json(request_body): Json, + Json(request_body): Json, ) -> Result { Organisation::create_campaign( id, diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index 5897dc27..22a16399 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -36,6 +36,7 @@ pub struct CampaignDetails { pub starts_at: DateTime, pub ends_at: DateTime, } + #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct OrganisationCampaign { pub id: i64, @@ -47,6 +48,15 @@ pub struct OrganisationCampaign { pub ends_at: DateTime, } +#[derive(Deserialize)] +pub struct NewCampaign { + pub slug: String, + pub name: String, + pub description: Option, + pub starts_at: DateTime, + pub ends_at: DateTime +} + #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct CampaignUpdate { pub slug: String, From 0e17bdb77ad6df7f540c326cb4082ef5de5d985d Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 19:31:09 +1100 Subject: [PATCH 16/20] block applications for ended campaigns --- backend/server/src/handler/campaign.rs | 3 ++- backend/server/src/models/campaign.rs | 33 +++++++++++++++++++++++++- backend/server/src/models/error.rs | 4 ++++ backend/server/src/service/campaign.rs | 22 +++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index 0e1db393..49671ad8 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -4,7 +4,7 @@ use crate::models::application::Application; use crate::models::application::NewApplication; use crate::models::auth::AuthUser; use crate::models::auth::CampaignAdmin; -use crate::models::campaign::Campaign; +use crate::models::campaign::{Campaign, OpenCampaign}; use crate::models::error::ChaosError; use crate::models::offer::Offer; use crate::models::role::{Role, RoleUpdate}; @@ -104,6 +104,7 @@ impl CampaignHandler { State(state): State, Path(id): Path, user: AuthUser, + _: OpenCampaign, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index 22a16399..6e8a3569 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -1,11 +1,16 @@ +use std::collections::HashMap; use chrono::{DateTime, Utc}; use s3::Bucket; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Transaction}; use sqlx::{Pool, Postgres}; use std::ops::DerefMut; +use axum::{async_trait, RequestPartsExt}; +use axum::extract::{FromRef, FromRequestParts, Path}; +use axum::http::request::Parts; use uuid::Uuid; - +use crate::models::app::AppState; +use crate::service::application::assert_application_is_open; use super::{error::ChaosError, storage::Storage}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] @@ -236,3 +241,29 @@ impl Campaign { Ok(()) } } + +pub struct OpenCampaign; + +#[async_trait] +impl FromRequestParts for OpenCampaign +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 campaign_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_application_is_open(application_id, &app_state.db).await?; + + Ok(OpenCampaign) + } +} diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 58ca6b11..c666f96e 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -22,6 +22,9 @@ pub enum ChaosError { #[error("Application closed")] ApplicationClosed, + #[error("Campagin closed")] + CampaignClosed, + #[error("SQLx error")] DatabaseError(#[from] sqlx::Error), @@ -70,6 +73,7 @@ impl IntoResponse for ChaosError { } ChaosError::BadRequest => (StatusCode::BAD_REQUEST, "Bad request").into_response(), ChaosError::ApplicationClosed => (StatusCode::BAD_REQUEST, "Application closed").into_response(), + ChaosError::CampaignClosed => (StatusCode::BAD_REQUEST, "Campaign closed").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/campaign.rs b/backend/server/src/service/campaign.rs index b9551473..fe041f96 100644 --- a/backend/server/src/service/campaign.rs +++ b/backend/server/src/service/campaign.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -28,3 +29,24 @@ pub async fn user_is_campaign_admin( Ok(()) } + +pub async fn assert_campaign_is_open( + campaign_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let campaign = sqlx::query!( + " + SELECT ends_at FROM campaigns WHERE id = $1 + ", + campaign_id + ) + .fetch_one(pool) + .await?; + + if campaign.ends_at <= time { + return Err(ChaosError::CampaignClosed) + } + + Ok(()) +} \ No newline at end of file From 36fd5c13ca02c224c05edca48b26f311cf0def68 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 19:40:15 +1100 Subject: [PATCH 17/20] `NewEmailTemplate` model for template request body --- backend/server/src/handler/organisation.rs | 4 ++-- backend/server/src/models/email_template.rs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index a317d1c5..d9c90566 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -2,7 +2,7 @@ use crate::models::app::AppState; use crate::models::auth::SuperUser; use crate::models::auth::{AuthUser, OrganisationAdmin}; use crate::models::campaign::{Campaign, NewCampaign}; -use crate::models::email_template::EmailTemplate; +use crate::models::email_template::{EmailTemplate, NewEmailTemplate}; use crate::models::error::ChaosError; use crate::models::organisation::{ AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck, @@ -198,7 +198,7 @@ impl OrganisationHandler { Path(id): Path, State(state): State, _admin: OrganisationAdmin, - Json(request_body): Json, + Json(request_body): Json, ) -> Result { Organisation::create_email_template( id, diff --git a/backend/server/src/models/email_template.rs b/backend/server/src/models/email_template.rs index ecf3792e..c1628f1f 100644 --- a/backend/server/src/models/email_template.rs +++ b/backend/server/src/models/email_template.rs @@ -23,6 +23,13 @@ pub struct EmailTemplate { pub template_body: String, } +#[derive(Deserialize, Serialize)] +pub struct NewEmailTemplate { + pub name: String, + pub template_subject: String, + pub template_body: String, +} + impl EmailTemplate { pub async fn get( id: i64, From 43eee89c52e7346e8d628c59fab5710c3c736c84 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 19:58:16 +1100 Subject: [PATCH 18/20] fix `assert_campaign_is_open()` naming --- backend/server/src/models/campaign.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index 6e8a3569..d8062327 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -10,7 +10,7 @@ use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use uuid::Uuid; use crate::models::app::AppState; -use crate::service::application::assert_application_is_open; +use crate::service::campaign::assert_campaign_is_open; use super::{error::ChaosError, storage::Storage}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] @@ -262,7 +262,7 @@ where .get("application_id") .ok_or(ChaosError::BadRequest)?; - assert_application_is_open(application_id, &app_state.db).await?; + assert_campaign_is_open(campaign_id, &app_state.db).await?; Ok(OpenCampaign) } From 266fbff7dc31cdaf0bf6efff767acfd17ed12200 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 19:59:48 +1100 Subject: [PATCH 19/20] api.yaml up to `/organisation/{id}/logo` --- backend/api.yaml | 476 +++++++++++++++++++++++++++++++---------------- 1 file changed, 319 insertions(+), 157 deletions(-) diff --git a/backend/api.yaml b/backend/api.yaml index 0e60bbd3..d0684e4b 100644 --- a/backend/api.yaml +++ b/backend/api.yaml @@ -77,12 +77,8 @@ paths: application/json: schema: $ref: "#/components/schemas/User" - "401": - description: Not logged in - content: - application/json: - schema: - $ref: "#/components/schemas/NotLoggedIn" + "307": + $ref: "#/components/responses/NotLoggedIn" /user/name: patch: operationId: updateUserName @@ -108,12 +104,8 @@ paths: message: type: string example: Successfully updated name - "401": - description: Not logged in. - content: - application/json: - schema: - $ref: "#/components/schemas/NotLoggedIn" + "307": + $ref: "#/components/responses/NotLoggedIn" /user/pronouns: patch: operationId: updateUserPronouns @@ -139,12 +131,8 @@ paths: message: type: string example: Successfully updated pronouns - "401": - description: Not logged in. - content: - application/json: - schema: - $ref: "#/components/schemas/NotLoggedIn" + "307": + $ref: "#/components/responses/NotLoggedIn" /user/gender: patch: operationId: updateUserGender @@ -170,12 +158,8 @@ paths: message: type: string example: Successfully updated gender - "401": - description: Not logged in. - content: - application/json: - schema: - $ref: "#/components/schemas/NotLoggedIn" + "307": + $ref: "#/components/responses/NotLoggedIn" /user/zid: patch: operationId: updateUserZid @@ -201,12 +185,8 @@ paths: message: type: string example: Successfully updated zID - "401": - description: Not logged in. - content: - application/json: - schema: - $ref: "#/components/schemas/NotLoggedIn" + "307": + $ref: "#/components/responses/NotLoggedIn" /user/degree: patch: operationId: updateUserDegree @@ -235,12 +215,8 @@ paths: message: type: string example: Successfully updated email - "401": - description: Not logged in. - content: - application/json: - schema: - $ref: "#/components/schemas/NotLoggedIn" + "307": + $ref: "#/components/responses/NotLoggedIn" /user/applications: get: operationId: getUserApplications @@ -258,12 +234,8 @@ paths: type: array items: $ref: "#/components/schemas/ApplicationDetails" - "401": - description: Not logged in. - content: - application/json: - schema: - $ref: "#/components/schemas/NotLoggedIn" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation: post: @@ -305,6 +277,8 @@ paths: application/json: schema: $ref: "#/components/schemas/Unauthorized" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/slug_check: post: operationId: checkOrganisationSlugAvailability @@ -336,6 +310,8 @@ paths: application/json: schema: $ref: "#/components/schemas/BadRequest" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}: get: operationId: getOrganisationById @@ -347,7 +323,7 @@ paths: schema: type: integer format: int64 - description: Returns info about specified organisation. + description: Returns info about specified organisation tags: - Organisation responses: @@ -356,29 +332,9 @@ paths: content: application/json: schema: - properties: - id: - type: integer - format: int64 - example: 6996987893965262849 - name: - type: string - example: UNSW Software Development Society - logo: - type: string - example: "76718252-2a13-4de2-bc07-f977c75dc52b" - created_at: - type: string - example: 2024-02-10T18:25:43.511Z - "401": - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. + $ref: "#/components/schemas/OrganisationDetails" + "307": + $ref: "#/components/responses/NotLoggedIn" delete: operationId: deleteOrganisationById parameters: @@ -389,7 +345,7 @@ paths: schema: type: integer format: int64 - description: Deletes specified organisation. + description: Deletes specified organisation tags: - Organisation responses: @@ -401,28 +357,111 @@ paths: properties: message: type: string - example: Successfully deleted organisation. - "401": - description: Not logged in. + example: Successfully deleted organisation + "403": + description: User is not a SuperUser + content: + application/json: + schema: + $ref: "#/components/schemas/Unauthorized" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/slug/{slug}: + get: + operationId: getOrganisationBySlug + parameters: + - name: slug + in: path + description: Organisation slug + required: true + schema: + type: string + description: Returns info about specified organisation + tags: + - Organisation + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/OrganisationDetails" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/campaign: + post: + operationId: createCampaign + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Create a new campaign inside specified organisation + tags: + - Organisation + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewCampaign" + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Not logged in. - "403": - description: User is not a super user. + example: Successfully created campaign + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/campaign/slug_check: + post: + operationId: checkCampaignSlugAvailability + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Checks availability of campaign slug in specified organisation + tags: + - Organisation + requestBody: + required: true + content: + application/json: + schema: + properties: + slug: + type: string + example: 2024-subcom-recruitment + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Unauthorized + example: Campaign slug is available + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}/campaigns: get: - operationId: getOrganisationCampaignsById + operationId: getAllOrganisationCampaigns parameters: - name: id in: path @@ -431,7 +470,7 @@ paths: schema: type: integer format: int64 - description: Returns active campaigns for specified organisation. + description: Returns all (active & ended) campaigns for specified organisation. However, ended campaigns cannot have new applications tags: - Organisation responses: @@ -445,16 +484,68 @@ paths: type: array items: $ref: "#/components/schemas/OrganisationCampaign" - "401": - description: Not logged in. + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/email_template: + post: + operationId: createEmailTemplate + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Create a new email template within the organisation + tags: + - Organisation + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewEmailTemplate" + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Not logged in. - + example: Successfully created email template + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/email_templates: + get: + operationId: getAllOrganisationEmailTemplates + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Get all email templates for specified organisation + tags: + - Organisation + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EmailTemplate" + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}/logo: patch: operationId: updateOrganisationLogoById @@ -466,7 +557,7 @@ paths: schema: type: integer format: int64 - description: Updates logo for specified organistion. + description: Update logo for specified organistion. Returns a PUT url to upload new image to tags: - Organisation responses: @@ -478,27 +569,12 @@ paths: properties: upload_url: type: string - description: Presigned S3 url to upload file. - example: https://www.youtube.com/watch?v=dQw4w9WgXcQ + description: Presigned S3 url to upload file + example: https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d "401": - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - "403": - description: User is not an organisation admin. - content: - application/json: - schema: - properties: - error: - type: string - example: Unauthorized - + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}/member: get: operationId: getOrganisationMembersById @@ -808,58 +884,6 @@ paths: error: type: string example: Unauthorized - - /organisation/{id}/campaign: - post: - operationId: createCampaign - parameters: - - name: id - in: path - description: Organisation ID - required: true - schema: - type: integer - format: int64 - description: Creates a new campaign inside specified organisation. - tags: - - Organisation - requestBody: - required: true - content: - application/json: - schema: - properties: - name: - type: string - example: 2024 Subcommittee Recruitment - description: - type: string - example: Are you excited to make a difference? - starts_at: - type: string - example: 2024-03-15T18:25:43.511Z - ends_at: - type: string - example: 2024-04-15T18:25:43.511Z - responses: - "200": - description: OK - content: - application/json: - schema: - properties: - message: - type: string - example: Successfully created campaign. - "403": - description: User is not an admin of specified organisation. - content: - application/json: - schema: - properties: - error: - type: string - example: Unauthorized /campaign: get: operationId: getAllCampaigns @@ -1066,7 +1090,7 @@ paths: upload_url: type: string description: Presigned S3 url to upload file. - example: https://www.youtube.com/watch?v=dQw4w9WgXcQ + example: https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d "401": description: Not logged in. content: @@ -1670,6 +1694,26 @@ components: - Rejected - Successful + OrganisationDetails: + type: object + properties: + id: + type: integer + format: int64 + example: 6996987893965262849 + slug: + type: string + example: devsoc-unsw + name: + type: string + example: UNSW Software Development Society + logo: + type: string + example: "76718252-2a13-4de2-bc07-f977c75dc52b" + created_at: + type: string + example: 2024-02-10T18:25:43.511Z + OrganisationCampaign: type: object properties: @@ -1677,6 +1721,9 @@ components: type: integer format: int64 example: 6996987893965262849 + slug: + type: string + example: 2024-subcom-recruitment name: type: string example: 2024 Subcommittee Recruitment @@ -1823,3 +1870,118 @@ components: type: integer format: int32 example: 1 + + NewCampaign: + type: object + properties: + name: + type: string + example: 2024 Subcommittee Recruitment + slug: + type: string + example: 2024-subcom-recruitment + description: + type: string + example: Are you excited to make a difference? + starts_at: + type: string + example: 2024-03-15T18:25:43.511Z + ends_at: + type: string + example: 2024-04-15T18:25:43.511Z + + Campaign: + type: object + properties: + id: + type: integer + format: int64 + example: 1541815603606036480 + slug: + type: string + example: 2024-subcom-recruitment + name: + type: string + example: 2024 Subcommittee Recruitment + organisation_id: + type: integer + format: int64 + example: 1541815603606036480 + organisation_name: + type: string + example: UNSW Software Development Society + cover_image: + type: string + format: uuid + example: 05ebad1e-8be4-40c3-9d36-140cac9a0075 + nullable: true + description: + type: string + example: Are you excited to make a difference? + nullable: true + starts_at: + type: string + example: 2024-03-15T18:25:43.511Z + ends_at: + type: string + example: 2024-04-15T18:25:43.511Z + created_at: + type: string + example: 2024-02-15T18:25:43.511Z + updated_at: + type: string + example: 2024-02-15T18:25:43.511Z + + EmailTemplate: + type: object + properties: + id: + type: integer + format: int64 + example: 1541815603606036480 + organisation_id: + type: integer + format: int64 + example: 1541815603606036480 + name: + type: string + example: Success Email + template_subject: + type: string + example: "[OUTCOME] {{campaign_name}} - {{role_name}}" + template_body: + type: string + example: "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + + NewEmailTemplate: + type: object + properties: + name: + type: string + example: Success Email + template_subject: + type: string + example: "[OUTCOME] {{campaign_name}} - {{role_name}}" + template_body: + type: string + example: "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + + responses: + NotLoggedIn: + description: Redirect to login + headers: + Location: + description: Login url + schema: + type: string + format: uri + + NotOrganisationAdmin: + description: User is not an organisation admin + content: + application/json: + schema: + properties: + error: + type: string + example: Unauthorized From 049a3933fea9189a1ab4ca4af52b70ffb62b833b Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 3 Dec 2024 20:05:17 +1100 Subject: [PATCH 20/20] update `api.json` up to `/organisation/{id}/logo` --- backend/api.json | 3742 +++++++++++++++++++++++++++++++++------------- 1 file changed, 2689 insertions(+), 1053 deletions(-) diff --git a/backend/api.json b/backend/api.json index 8be962fe..9cb1f9d1 100644 --- a/backend/api.json +++ b/backend/api.json @@ -1,1220 +1,1022 @@ { - "openapi": "3.0.0", - "info": { - "title": "Chaos API", - "version": "1.0.0" + "openapi": "3.0.0", + "info": { + "title": "Chaos API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://chaos.csesoc.app/api", + "description": "Production server" }, - "servers": [ - { - "url": "https://chaos.csesoc.app/api", - "description": "Production server" - }, - { - "url": "http://localhost:3000/api", - "description": "Local server" - } - ], - "paths": { - "/auth/logout": { - "post": { - "operationId": "logout", - "description": "Invalidates current token.", - "tags": [ - "Auth" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "messages": { - "type": "string", - "example": "Successfully logged out." - } - } - } + { + "url": "http://localhost:3000/api", + "description": "Local server" + } + ], + "paths": { + "/": { + "get": { + "operationId": "getRoot", + "description": "Root of API", + "tags": ["Miscellaneous"], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string", + "example": "Join DevSoc! https://devsoc.app/" } } } } } - }, - "/user": { - "get": { - "operationId": "getLoggedInUser", - "description": "Returns info about currently logged in user.", - "tags": [ - "User" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500 - }, - "email": { - "type": "string", - "example": "me@example.com" - }, - "zid": { - "type": "string", - "example": "z5555555" - }, - "name": { - "type": "string", - "example": "Clancy Lion" - }, - "degree_name": { - "type": "string", - "example": "Computer Science" - }, - "degree_starting_year": { - "type": "integer", - "example": 2024 - }, - "role": { - "type": "string", - "example": "User" - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } - } - } - } + } + }, + "/auth/callback/google": { + "get": { + "operationId": "googleCallback", + "description": "Google OAuth callback", + "tags": ["Auth"], + "parameters": [ + { + "name": "code", + "in": "query", + "description": "Google OAuth code", + "required": true, + "schema": { + "type": "string" } } - }, - "delete": { - "operationId": "deleteUserById", - "description": "Deletes currently logged in user.", - "tags": [ - "User" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully deleted user." - } + ], + "responses": { + "200": { + "description": "Ok", + "content": null + } + } + } + }, + "/auth/logout": { + "post": { + "operationId": "logout", + "description": "Invalidates current token", + "tags": ["Auth"], + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully logged out" } } } } - }, - "401": { - "description": "Not logged.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } - } + } + }, + "401": { + "description": "Not logged in", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotLoggedIn" } } - }, - "403": { - "description": "User is only admin of an organisation.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Cannot delete sole admin of an organisation." - } - } - } + } + } + } + } + }, + "/user": { + "get": { + "operationId": "getLoggedInUser", + "description": "Returns info about currently logged in user", + "tags": ["User"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/user/{id}": { - "get": { - "operationId": "getUserById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "User ID", - "required": true, + } + }, + "/user/name": { + "patch": { + "operationId": "updateUserName", + "description": "Updates currently logged in user's name", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns info about specified user.", - "tags": [ - "User" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500 - }, - "email": { - "type": "string", - "example": "me@example.com" - }, - "zid": { - "type": "string", - "example": "z5555555" - }, - "name": { - "type": "string", - "example": "Clancy Lion" - }, - "degree_name": { - "type": "string", - "example": "Computer Science" - }, - "degree_starting_year": { - "type": "integer", - "example": 2024 - } - } - } - } - } - }, - "403": { - "description": "Requested user has not applied to one of authorized user's campaign.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Insufficient permissions" - } - } + "properties": { + "name": { + "type": "string", + "example": "Clancy Tiger" } } } } } - } - }, - "/user/name": { - "patch": { - "operationId": "updateUserName", - "description": "Updates currently logged in user's name.", - "tags": [ - "User" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { + "message": { "type": "string", - "example": "Clancy Tiger" + "example": "Successfully updated name" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated name." - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/user/pronouns": { + "patch": { + "operationId": "updateUserPronouns", + "description": "Updates currently logged in user's pronouns", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "pronouns": { + "type": "string", + "example": "They/Them" } } } } } - } - }, - "/user/zid": { - "patch": { - "operationId": "updateUserZid", - "description": "Updates currently logged in user's zID.", - "tags": [ - "User" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "zid": { + "message": { "type": "string", - "example": "z5123456" + "example": "Successfully updated pronouns" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated zID." - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/user/gender": { + "patch": { + "operationId": "updateUserGender", + "description": "Updates currently logged in user's gender", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "gender": { + "type": "string", + "example": "Female" } } } } } - } - }, - "/user/degree": { - "patch": { - "operationId": "updateUserDegree", - "description": "Updates currently logged in user's degree.", - "tags": [ - "User" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "degree_name": { + "message": { "type": "string", - "example": "Electrical Engineering" - }, - "degree_starting_year": { - "type": "integer", - "example": 2024 + "example": "Successfully updated gender" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated email." - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/user/zid": { + "patch": { + "operationId": "updateUserZid", + "description": "Updates currently logged in user's zID", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "zid": { + "type": "string", + "example": "z5123456" } } } } } - } - }, - "/organisation": { - "post": { - "operationId": "createOrganisation", - "description": "Creates a new organisation.", - "tags": [ - "Organisation" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { + "message": { "type": "string", - "example": "UNSW Software Development Society" - }, - "admin": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500, - "description": "User ID of admin" + "example": "Successfully updated zID" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully created organisation." - } - } - } - } - } - }, - "403": { - "description": "User is not a super user.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } - } - } - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/organisation/{id}": { - "get": { - "operationId": "getOrganisationById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, + } + }, + "/user/degree": { + "patch": { + "operationId": "updateUserDegree", + "description": "Updates currently logged in user's degree", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns info about specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "name": { - "type": "string", - "example": "UNSW Software Development Society" - }, - "logo": { - "type": "string", - "example": "76718252-2a13-4de2-bc07-f977c75dc52b" - }, - "created_at": { - "type": "string", - "example": "2024-02-10T18:25:43.511Z" - } - } + "properties": { + "degree_name": { + "type": "string", + "example": "Electrical Engineering" + }, + "degree_starting_year": { + "type": "integer", + "example": 2024 } } } } } }, - "delete": { - "operationId": "deleteOrganisationById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Deletes specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully deleted organisation." - } - } - } - } - } - }, - "403": { - "description": "User is not a super user.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated email" } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/organisation/{id}/campaigns": { - "get": { - "operationId": "getOrganisationCampaignsById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns active campaigns for specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "campaigns": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "cover_image": { - "type": "string", - "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" - } - } - } + } + }, + "/user/applications": { + "get": { + "operationId": "getUserApplications", + "description": "Returns info about applications made by currently logged in user", + "tags": ["User"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationDetails" } } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/organisation/{id}/logo": { - "patch": { - "operationId": "updateOrganisationLogoById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, + } + }, + "/organisation": { + "post": { + "operationId": "createOrganisation", + "description": "Create a new organisation", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Updates logo for specified organistion.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "upload_url": { - "type": "string", - "description": "Presigned S3 url to upload file.", - "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJJWZ7B6WCRGMKFGQ%2F20180210%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20180210T171315Z&X-Amz-Expires=1800&X-Amz-Signature=12b74b0788aa036bc7c3d03b3f20c61f1f91cc9ad8873e3314255dc479a25351&X-Amz-SignedHeaders=host" - } - } + "properties": { + "slug": { + "type": "string", + "description": "ASCII string for URL like https://chaos.csesoc.app/s/unsw-devsoc", + "example": "unsw-devsoc" + }, + "name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "admin": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500, + "description": "User ID of admin" } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created organisation" } } } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } - } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "403": { + "description": "User is not a SuperUser", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Unauthorized" } } } } } - }, - "/organisation/{id}/members": { - "get": { - "operationId": "getOrganisationMembersById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, + } + }, + "/organisation/slug_check": { + "post": { + "operationId": "checkOrganisationSlugAvailability", + "description": "Check if slug is available", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns list of members of specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "members": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500 - }, - "name": { - "type": "string", - "example": "Clancy Lion" - }, - "role": { - "type": "string", - "example": "Admin" - } - } - } - } - } - } - } - } - }, - "403": { - "description": "User is not an organisation admin or member.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } + "properties": { + "slug": { + "type": "string", + "example": "unsw-devsoc" } } } } } }, - "put": { - "operationId": "updateOrganisationMembersById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "members": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "integer", - "format": "int64" - }, - "example": [ - 1541815603606036500, - 1541815603606036700, - 1541815287306036500 - ] - } - } - } - } - } - }, - "description": "Specifies members for specified organistion.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated members." - } + "message": { + "type": "string", + "example": "Organisation slug is available" } } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } - } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "400": { + "description": "Bad request - slug is in use or not ASCII", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequest" } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } - } + } + } + } + } + }, + "/organisation/{id}": { + "get": { + "operationId": "getOrganisationById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationDetails" } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } }, - "/organisation/{id}/campaign": { - "post": { - "operationId": "createCampaign", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Creates a new campaign inside specified organisation.", - "tags": [ - "Organisation" - ], - "requestBody": { + "delete": { + "operationId": "deleteOrganisationById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Deletes specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { + "message": { "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" + "example": "Successfully deleted organisation" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully created campaign." - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "403": { + "description": "User is not a SuperUser", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Unauthorized" + } + } + } + } + } + } + }, + "/organisation/slug/{slug}": { + "get": { + "operationId": "getOrganisationBySlug", + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "Organisation slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "description": "Returns info about specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationDetails" + } + } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/organisation/{id}/campaign": { + "post": { + "operationId": "createCampaign", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Create a new campaign inside specified organisation", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewCampaign" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created campaign" } } } } - }, - "403": { - "description": "User is not an admin of specified organisation.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" + } + } + } + }, + "/organisation/{id}/campaign/slug_check": { + "post": { + "operationId": "checkCampaignSlugAvailability", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Checks availability of campaign slug in specified organisation", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Campaign slug is available" } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" } } - }, - "/campaign": { - "get": { - "operationId": "getAllCampaigns", - "description": "Returns all active campaigns.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "campaigns": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "organisation_id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036700 - }, - "organisation_name": { - "type": "string", - "example": "UNSW Software Development Society" - }, - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "cover_image": { - "type": "string", - "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" - } - } - } + } + }, + "/organisation/{id}/campaigns": { + "get": { + "operationId": "getAllOrganisationCampaigns", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns all (active & ended) campaigns for specified organisation. However, ended campaigns cannot have new applications", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganisationCampaign" } } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/campaign/{id}": { - "get": { - "operationId": "getCampaignById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, + } + }, + "/organisation/{id}/email_template": { + "post": { + "operationId": "createEmailTemplate", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Create a new email template within the organisation", + "tags": ["Organisation"], + "requestBody": { + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns info about specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "organisation_id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036700 - }, - "organisation_name": { - "type": "string", - "example": "UNSW Software Development Society" - }, - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "cover_image": { - "type": "string", - "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" - } + "$ref": "#/components/schemas/NewEmailTemplate" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created email template" } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" } - }, - "put": { - "operationId": "updateCampaignById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" + } + } + }, + "/organisation/{id}/email_templates": { + "get": { + "operationId": "getAllOrganisationEmailTemplates", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Get all email templates for specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmailTemplate" + } + } } } - ], - "requestBody": { + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" + } + } + } + }, + "/organisation/{id}/logo": { + "patch": { + "operationId": "updateOrganisationLogoById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Update logo for specified organistion. Returns a PUT url to upload new image to", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { + "upload_url": { "type": "string", - "example": "2024-04-15T18:25:43.511Z" + "description": "Presigned S3 url to upload file", + "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d" } } } } } }, - "description": "Updates details of specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated campaign." + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" + } + } + } + }, + "/organisation/{id}/member": { + "get": { + "operationId": "getOrganisationMembersById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns list of members of specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "role": { + "type": "string", + "example": "Admin" + } + } } } } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } + } + }, + "403": { + "description": "User is not an organisation admin or member.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" } } } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } + } + } + } + }, + "put": { + "operationId": "updateOrganisationMembersById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer", + "format": "int64" + }, + "example": [ + 1541815603606036500, 1541815603606036700, + 1541815287306036500 + ] } } } } } }, - "delete": { - "operationId": "deleteCampaignById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Deletes specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully deleted campaign." - } + "description": "Specifies members for specified organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated members." } } } } - }, - "403": { - "description": "User is not an admin of campaign's organisation.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" } } } @@ -1223,74 +1025,1908 @@ } } }, - "/campaign/{id}/banner": { - "patch": { - "operationId": "updateCampaignBannerById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, + "delete": { + "operationId": "deleteOrganisationMemberById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Updates banner image for specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "upload_url": { - "type": "string", - "description": "Presigned S3 url to upload file.", - "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJJWZ7B6WCRGMKFGQ%2F20180210%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20180210T171315Z&X-Amz-Expires=1800&X-Amz-Signature=12b74b0788aa036bc7c3d03b3f20c61f1f91cc9ad8873e3314255dc479a25351&X-Amz-SignedHeaders=host" - } + "properties": { + "user_id": { + "type": "integer", + "format": "int64" + } + } + } + } + } + }, + "description": "Specifies member for deletion in organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated members." } } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" } } } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" + } + } + } + } + }, + "/organisation/{id}/admin": { + "get": { + "operationId": "getOrganisationAdminsById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns list of admins of specified organisation.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "role": { + "type": "string", + "example": "Admin" + } + } } } } } } } + }, + "403": { + "description": "User is not a SuperUser.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "put": { + "operationId": "updateOrganisationAdminsById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer", + "format": "int64" + }, + "example": [ + 1541815603606036500, 1541815603606036700, + 1541815287306036500 + ] + } + } + } + } + } + }, + "description": "Specifies Admins for specified organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated members." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a SuperUser.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "deleteOrganisationAdminById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "user_id": { + "type": "integer", + "format": "int64" + } + } + } + } + } + }, + "description": "Specifies Admin for deletion in organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted Admin." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a SuperUser.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign": { + "get": { + "operationId": "getAllCampaigns", + "description": "Returns all active campaigns.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036700 + }, + "organisation_name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "cover_image": { + "type": "string", + "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + } + } + } + } + } + } + } + } + } + }, + "/campaign/{id}": { + "get": { + "operationId": "getCampaignById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036700 + }, + "organisation_name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "cover_image": { + "type": "string", + "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + } + } + } + } + } + }, + "put": { + "operationId": "updateCampaignById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + } + } + } + }, + "description": "Updates details of specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated campaign." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "deleteCampaignById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Deletes specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted campaign." + } + } + } + } + } + }, + "403": { + "description": "User is not an admin of campaign's organisation.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/banner": { + "patch": { + "operationId": "updateCampaignBannerById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Updates banner image for specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "upload_url": { + "type": "string", + "description": "Presigned S3 url to upload file.", + "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d" + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/role": { + "post": { + "operationId": "createRole", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Creates a new role in a campaign.", + "tags": ["Campaign"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "Chief Mouser" + }, + "description": { + "type": "string", + "required": false, + "example": "Larry the cat is dead, now we need someone else to handle the rat issues at 10th Downing st." + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": false + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created organisation." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/roles": { + "get": { + "operationId": "getRolesByCampaignId", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about all roles in a campaign", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "$ref": "#components/schemas/RoleDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/applications": { + "get": { + "operationId": "getApplicationsById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about all Applications in given Campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#components/schemas/ApplicationDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/role/{id}": { + "get": { + "operationId": "getRoleById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about specified role.", + "tags": ["Role"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "Chief Mouser" + }, + "description": { + "type": "string", + "example": "Larry the cat gone missing! now we need someone else to handle the rat issues at 10th Downing st." + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": false + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + } + } + }, + "put": { + "operationId": "updateRoleById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Update a role given the role id.", + "tags": ["Role"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "Chief Whip" + }, + "description": { + "type": "string", + "required": false, + "example": "Put a bit of stick about!" + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully update organisation." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "deleteRoleById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Deletes specified role.", + "tags": ["Role"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted role." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an admin of role's Campaign.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/role/{id}/applications": { + "get": { + "operationId": "getApplicationsByRoleID", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns all applications to a specific role", + "tags": ["Role"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#components/schemas/ApplicationDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/application/{id}": { + "get": { + "operationId": "getApplicationByID", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Application ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns an applications given its ID", + "tags": ["Application"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "application": { + "type": { + "$ref": "#components/schemas/ApplicationDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/application/{id}/private": { + "put": { + "operationId": "updateApplicationPrivateStatus", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Application ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": { + "$ref": "#components/schemas/ApplicationStatus" + } + } + } + } + } + } + }, + "description": "Change Private Status of a specific Application", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated Application Private Status." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/application/{id}/status": { + "put": { + "operationId": "updateApplicationStatus", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Application ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": { + "$ref": "#components/schemas/ApplicationStatus" + } + } + } + } + } + } + }, + "description": "Change Status of a specific Application", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated Application Status." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NotLoggedIn": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Not logged in" + } + } + }, + "Unauthorized": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + }, + "BadRequest": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Bad request" + } + } + }, + "Answer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965227000 + }, + "question_id": { + "type": "integer", + "format": "int64", + "example": 6996987893965227000 + }, + "answer_type": { + "type": "string", + "enum": [ + "ShortAnswer", + "MultiChoice", + "MultiSelect", + "DropDown", + "Ranking" + ] + }, + "data": { + "oneOf": [ + { + "type": "string", + "example": "I am passionate about events" + }, + { + "type": "integer", + "example": 6996987893965227000 + }, + { + "type": "array", + "format": "int64", + "example": [ + 6996987893965227000, 69969829832652230000, 6996987893965228000 + ] + } + ] + }, + "created_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "updated_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + } + } + }, + "NewAnswer": { + "type": "object", + "properties": { + "question_id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "answer_type": { + "type": "string", + "enum": [ + "ShortAnswer", + "MultiChoice", + "MultiSelect", + "DropDown", + "Ranking" + ] + }, + "data": { + "oneOf": [ + { + "type": "string", + "example": "I am passionate about events" + }, + { + "type": "integer", + "example": 6996987893965227000 + }, + { + "type": "array", + "format": "int64", + "example": [ + 6996987893965227000, 69969829832652230000, 6996987893965228000 + ] + } + ] + } + } + }, + "ApplicationStatus": { + "type": "string", + "enum": ["Pending", "Rejected", "Successful"] + }, + "OrganisationDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "slug": { + "type": "string", + "example": "devsoc-unsw" + }, + "name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "logo": { + "type": "string", + "example": "76718252-2a13-4de2-bc07-f977c75dc52b" + }, + "created_at": { + "type": "string", + "example": "2024-02-10T18:25:43.511Z" + } + } + }, + "OrganisationCampaign": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "cover_image": { + "type": "string", + "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + }, + "RoleDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 7036987893965263000 + }, + "campaign_id": { + "type": "integer", + "format": "int64", + "example": 1116987453965262800 + }, + "name": { + "type": "string", + "example": "Chief Mouser" + }, + "description": { + "type": "string", + "example": "Larry the cat gone missing! now we need someone else to handle the rat issues at 10th Downing st." + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": false + } + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "email": { + "type": "string", + "example": "me@example.com" + }, + "zid": { + "type": "string", + "example": "z5555555", + "nullable": true + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "pronouns": { + "type": "string", + "example": "He/Him" + }, + "gender": { + "type": "string", + "example": "Male" + }, + "degree_name": { + "type": "string", + "example": "Computer Science", + "nullable": true + }, + "degree_starting_year": { + "type": "integer", + "example": 2024, + "nullable": true + }, + "role": { + "type": "string", + "enum": ["User", "SuperUser"] + } + } + }, + "UserDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "email": { + "type": "string", + "example": "me@example.com" + }, + "zid": { + "type": "string", + "example": "z5555555" + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "pronouns": { + "type": "string", + "example": "They/Them" + }, + "gender": { + "type": "string", + "example": "Male" + }, + "degree_name": { + "type": "string", + "example": "Computer Science" + }, + "degree_starting_year": { + "type": "integer", + "example": 2024 + } + } + }, + "ApplicationDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "campaign_id": { + "type": "integer", + "format": "int64", + "example": 5141815603606036000 + }, + "user": { + "$ref": "#/components/schemas/UserDetails" + }, + "status": { + "$ref": "#/components/schemas/ApplicationStatus" + }, + "private_status": { + "$ref": "#/components/schemas/ApplicationStatus" + }, + "applied_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAppliedRoleDetails" + } + } + } + }, + "ApplicationAppliedRoleDetails": { + "type": "object", + "properties": { + "campaign_role_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "role_name": { + "type": "String", + "example": "Sponsorships" + }, + "preference": { + "type": "integer", + "format": "int32", + "example": 1 + } + } + }, + "NewCampaign": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + }, + "Campaign": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "organisation_name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "cover_image": { + "type": "string", + "format": "uuid", + "example": "05ebad1e-8be4-40c3-9d36-140cac9a0075", + "nullable": true + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?", + "nullable": true + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + }, + "created_at": { + "type": "string", + "example": "2024-02-15T18:25:43.511Z" + }, + "updated_at": { + "type": "string", + "example": "2024-02-15T18:25:43.511Z" + } + } + }, + "EmailTemplate": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "name": { + "type": "string", + "example": "Success Email" + }, + "template_subject": { + "type": "string", + "example": "[OUTCOME] {{campaign_name}} - {{role_name}}" + }, + "template_body": { + "type": "string", + "example": "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + } + } + }, + "NewEmailTemplate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Success Email" + }, + "template_subject": { + "type": "string", + "example": "[OUTCOME] {{campaign_name}} - {{role_name}}" + }, + "template_body": { + "type": "string", + "example": "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + } + } + } + }, + "responses": { + "NotLoggedIn": { + "description": "Redirect to login", + "headers": { + "Location": { + "description": "Login url", + "schema": { + "type": "string", + "format": "uri" + } + } + } + }, + "NotOrganisationAdmin": { + "description": "User is not an organisation admin", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } } } } } - } \ No newline at end of file + } +}