Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Email Sending & Application Lockouts #530

Draft
wants to merge 20 commits into
base: CHAOS-224-KHAOS-rewrite
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5db09ad
email sending
KavikaPalletenne Dec 2, 2024
5a163d3
fix "recipient" spelling
KavikaPalletenne Dec 2, 2024
07680cc
cargo fmt
KavikaPalletenne Dec 2, 2024
7c3d624
fix errors with `template_subject` introduction
KavikaPalletenne Dec 2, 2024
5aeeb2f
cargo clippy fixes
KavikaPalletenne Dec 2, 2024
706f814
application role preferences and updating applied roles
KavikaPalletenne Dec 2, 2024
299ee9b
lock out user from application changes after submission
KavikaPalletenne Dec 2, 2024
43e47cd
lock application after submission and campaign close date
KavikaPalletenne Dec 2, 2024
80c1abe
fix uses of `LEFT JOIN` when `JOIN` was needed
KavikaPalletenne Dec 2, 2024
efe6c00
make unsubmitted application viewable after campaign end
KavikaPalletenne Dec 2, 2024
02b705b
only submitted applications are viewable by reviewers
KavikaPalletenne Dec 2, 2024
6fcb06a
added `Answer`-related schemas to api.yaml
KavikaPalletenne Dec 2, 2024
c08e4b4
register new `ApplicationHandler` endpoints in app
KavikaPalletenne Dec 3, 2024
d2d402a
completed up to /organisation/slug_check
KavikaPalletenne Dec 3, 2024
b2b7426
`NewCampaign` model for campaign create request body
KavikaPalletenne Dec 3, 2024
0e17bdb
block applications for ended campaigns
KavikaPalletenne Dec 3, 2024
36fd5c1
`NewEmailTemplate` model for template request body
KavikaPalletenne Dec 3, 2024
43eee89
fix `assert_campaign_is_open()` naming
KavikaPalletenne Dec 3, 2024
266fbff
api.yaml up to `/organisation/{id}/logo`
KavikaPalletenne Dec 3, 2024
049a393
update `api.json` up to `/organisation/{id}/logo`
KavikaPalletenne Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,742 changes: 2,689 additions & 1,053 deletions backend/api.json

Large diffs are not rendered by default.

1,034 changes: 631 additions & 403 deletions backend/api.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/migrations/20240406031915_create_applications.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -24,6 +25,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)
Expand Down
3 changes: 2 additions & 1 deletion backend/migrations/20241124054711_email_templates.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions backend/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
4 changes: 4 additions & 0 deletions backend/server/src/handler/answer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,6 +16,7 @@ impl AnswerHandler {
State(state): State<AppState>,
Path(application_id): Path<i64>,
_user: ApplicationOwner,
_: OpenApplicationByApplicationId,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewAnswer>,
) -> Result<impl IntoResponse, ChaosError> {
Expand Down Expand Up @@ -63,6 +65,7 @@ impl AnswerHandler {
pub async fn update(
Path(answer_id): Path<i64>,
_owner: AnswerOwner,
_: OpenApplicationByAnswerId,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewAnswer>,
) -> Result<impl IntoResponse, ChaosError> {
Expand All @@ -76,6 +79,7 @@ impl AnswerHandler {
pub async fn delete(
Path(answer_id): Path<i64>,
_owner: AnswerOwner,
_: OpenApplicationByAnswerId,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
Answer::delete(answer_id, &mut transaction.tx).await?;
Expand Down
26 changes: 24 additions & 2 deletions backend/server/src/handler/application.rs
Original file line number Diff line number Diff line change
@@ -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, OpenApplicationByApplicationId};
use crate::models::auth::{ApplicationAdmin, ApplicationOwner, AuthUser};
use crate::models::error::ChaosError;
use crate::models::transaction::DBTransaction;
use axum::extract::{Json, Path, State};
Expand Down Expand Up @@ -48,4 +48,26 @@ impl ApplicationHandler {
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(applications)))
}

pub async fn update_roles(
_user: ApplicationOwner,
Path(application_id): Path<i64>,
mut transaction: DBTransaction<'_>,
Json(data): Json<ApplicationRoleUpdate>,
) -> Result<impl IntoResponse, ChaosError> {
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,
_: OpenApplicationByApplicationId,
Path(application_id): Path<i64>,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
Application::submit(application_id, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, "Successfully submitted application"))
}
}
3 changes: 2 additions & 1 deletion backend/server/src/handler/campaign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -104,6 +104,7 @@ impl CampaignHandler {
State(state): State<AppState>,
Path(id): Path<i64>,
user: AuthUser,
_: OpenCampaign,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewApplication>,
) -> Result<impl IntoResponse, ChaosError> {
Expand Down
9 changes: 8 additions & 1 deletion backend/server/src/handler/email_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ impl EmailTemplateHandler {
State(state): State<AppState>,
Json(request_body): Json<EmailTemplate>,
) -> Result<impl IntoResponse, ChaosError> {
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"))
}
Expand Down
10 changes: 6 additions & 4 deletions backend/server/src/handler/offer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::models::app::AppState;
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;

Expand Down Expand Up @@ -47,18 +48,19 @@ impl OfferHandler {
Path(id): Path<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
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(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: OfferAdmin,
State(state): State<AppState>,
) -> Result<impl IntoResponse, ChaosError> {
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"))
Expand Down
12 changes: 6 additions & 6 deletions backend/server/src/handler/organisation.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use crate::models;
use crate::models::app::AppState;
use crate::models::auth::SuperUser;
use crate::models::auth::{AuthUser, OrganisationAdmin};
use crate::models::campaign::Campaign;
use crate::models::email_template::EmailTemplate;
use crate::models::campaign::{Campaign, NewCampaign};
use crate::models::email_template::{EmailTemplate, NewEmailTemplate};
use crate::models::error::ChaosError;
use crate::models::organisation::{
AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck,
Expand Down Expand Up @@ -167,7 +166,7 @@ impl OrganisationHandler {
Path(id): Path<i64>,
State(state): State<AppState>,
_admin: OrganisationAdmin,
Json(request_body): Json<Campaign>,
Json(request_body): Json<NewCampaign>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::create_campaign(
id,
Expand Down Expand Up @@ -199,12 +198,13 @@ impl OrganisationHandler {
Path(id): Path<i64>,
State(state): State<AppState>,
_admin: OrganisationAdmin,
Json(request_body): Json<models::email_template::EmailTemplate>,
Json(request_body): Json<NewEmailTemplate>,
) -> Result<impl IntoResponse, ChaosError> {
Organisation::create_email_template(
id,
request_body.name,
request_body.template,
request_body.template_subject,
request_body.template_body,
&state.db,
state.snowflake_generator,
)
Expand Down
11 changes: 5 additions & 6 deletions backend/server/src/models/answer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ pub struct Answer {

#[derive(Deserialize)]
pub struct NewAnswer {
pub application_id: i64,
pub question_id: i64,

#[serde(flatten)]
Expand Down Expand Up @@ -320,7 +319,7 @@ impl Answer {
id: i64,
transaction: &mut Transaction<'_, Postgres>,
) -> Result<(), ChaosError> {
sqlx::query!("DELETE FROM answers WHERE id = $1 RETURNING id", id)
let _ = sqlx::query!("DELETE FROM answers WHERE id = $1 RETURNING id", id)
.fetch_one(transaction.deref_mut())
.await?;

Expand Down Expand Up @@ -355,7 +354,7 @@ impl AnswerData {
multi_option_answers: Option<Vec<i64>>,
ranking_answers: Option<Vec<i64>>,
) -> Self {
return match question_type {
match question_type {
QuestionType::ShortAnswer => {
let answer =
short_answer_answer.expect("Data should exist for ShortAnswer variant");
Expand All @@ -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);
}
}
Expand Down
14 changes: 14 additions & 0 deletions backend/server/src/models/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<Router, ChaosError> {
Expand Down Expand Up @@ -65,6 +67,9 @@ pub async fn app() -> Result<Router, ChaosError> {
// 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,
Expand All @@ -75,6 +80,7 @@ pub async fn app() -> Result<Router, ChaosError> {
jwt_validator,
snowflake_generator,
storage_bucket,
email_credentials,
};

Ok(Router::new()
Expand Down Expand Up @@ -242,6 +248,14 @@ pub async fn app() -> Result<Router, ChaosError> {
"/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),
Expand Down
Loading
Loading