From 5d1361267b9558185777dac37a064fe6548589b3 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Manningham <marc-antoine.m@outlook.com> Date: Wed, 23 Oct 2024 12:52:19 -0400 Subject: [PATCH] feat: cleanup universities into an enum --- TODO.md | 2 - flake.lock | 61 +++++++++++++++++++ flake.nix | 12 ++-- server/launch_db.sh | 10 +-- ...241015182258_create_participants_table.sql | 3 + server/src/auth/claims.rs | 8 +-- server/src/bin/create_admin.rs | 10 +-- server/src/model/minimal_participant.rs | 12 ++-- server/src/model/mod.rs | 2 +- server/src/model/participant.rs | 24 ++++++-- server/src/model/preview_participant.rs | 4 +- server/src/model/staff.rs | 0 server/src/model/university.rs | 23 +++++++ server/src/routes/get_participant.rs | 50 +++++++++++++++ server/src/routes/get_participants.rs | 7 +-- server/src/routes/mod.rs | 1 + server/src/routes/new_participant.rs | 11 ++-- 17 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 flake.lock delete mode 100644 server/src/model/staff.rs create mode 100644 server/src/model/university.rs diff --git a/TODO.md b/TODO.md index 6adfd3b..9cb72f3 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,6 @@ -- Finish Autentification and route protection with JWT - Implement detailed view of Participant information - Password reset after account creation - Client side field validation -- Deployment - Traduction - Search functionality diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b706e5a --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1729413321, + "narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 5a30df4..c93f588 100644 --- a/flake.nix +++ b/flake.nix @@ -29,12 +29,12 @@ packages = with pkgs; [ bun ]; - DB_USER="postgres"; - DB_PASSWORD="password"; - DB_NAME="dev"; - DB_PORT="5432"; - DB_HOST="localhost"; - DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"; + POSTGRES_USER="postgres"; + POSTGRES_PASSWORD="password"; + POSTGRES_NAME="dev"; + POSTGRES_PORT="5432"; + POSTGRES_HOST="localhost"; + DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_NAME}"; TS_RS_EXPORT_DIR = "../client/src/binding"; LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.openssl ]; diff --git a/server/launch_db.sh b/server/launch_db.sh index 82ab166..3743a2e 100755 --- a/server/launch_db.sh +++ b/server/launch_db.sh @@ -14,17 +14,17 @@ set -x set -eo pipefail docker run \ - -e POSTGRES_USER=${DB_USER} \ - -e POSTGRES_PASSWORD=${DB_PASSWORD} \ - -p "${DB_PORT}":5432 \ + -e POSTGRES_USER=${POSTGRES_USER} \ + -e POSTGRES_PASSWORD=${POSTGRES_PASSWORD} \ + -p "${POSTGRES_PORT}":5432 \ -d postgres -export PGPASSWORD=${DB_PASSWORD} +export PGPASSWORD=${POSTGRES_PASSWORD} until psql -h "${POSTGRES_HOST}" -U "${POSTGRES_USER}" -p "${POSTGRES_PORT}" -d "postgres" -c '\q'; do >&2 echo "Postgres is unavailable - sleeping" sleep 1 done ->&2 echo "Postgres is up and running on port ${DB_PORT}" +>&2 echo "Postgres is up and running on port ${POSTGRES_PORT}" bash configure_db.sh diff --git a/server/migrations/20241015182258_create_participants_table.sql b/server/migrations/20241015182258_create_participants_table.sql index 3c404ab..50c68da 100644 --- a/server/migrations/20241015182258_create_participants_table.sql +++ b/server/migrations/20241015182258_create_participants_table.sql @@ -2,6 +2,8 @@ CREATE TYPE ROLE AS ENUM ('participant', 'organizer', 'volunteer', 'chef'); CREATE TYPE COMPETITION AS ENUM ('none', 'conception_senior', 'conception_junior', 'debats_oratoires', 'reingenierie', 'genie_conseil', 'communication_scientifique', 'programmation', 'conception_innovatrice', 'cycle_superieur'); +CREATE TYPE UNIVERSITY AS ENUM ('uqac', 'uqar', 'uqat', 'uqo', 'uqtr', 'mcgill', 'mcgill_macdonald', 'concordia', 'ets', 'polymtl', 'ulaval', 'ulaval-agriculture', 'uds', 'none'); + CREATE TABLE participants ( id UUID PRIMARY KEY, role ROLE NOT NULL, @@ -14,6 +16,7 @@ CREATE TABLE participants ( medical_conditions TEXT, allergies TEXT, supper TEXT, + is_vegetarian BOOLEAN, pronouns TEXT, phone_number TEXT, tshirt_size TEXT, diff --git a/server/src/auth/claims.rs b/server/src/auth/claims.rs index d6cd920..cad2df0 100644 --- a/server/src/auth/claims.rs +++ b/server/src/auth/claims.rs @@ -1,4 +1,4 @@ -use crate::model::role::Role; +use crate::model::{role::Role, university::University}; use argon2::{password_hash::PasswordVerifier, Argon2, PasswordHash}; use axum::{async_trait, extract::FromRequestParts, http::request::Parts, RequestPartsExt}; use axum_extra::{ @@ -19,7 +19,7 @@ use super::auth_error::AuthError; pub struct Claims { pub id: Uuid, pub role: Role, - pub university: String, + pub university: University, pub exp: usize, } @@ -33,7 +33,7 @@ impl Claims { .and_utc() .timestamp() as usize; let info = sqlx::query!( - r#"SELECT id, role AS "role: Role", university FROM participants WHERE email = $1"#, + r#"SELECT id, role AS "role: Role", university AS "university: University" FROM participants WHERE email = $1"#, email ) .fetch_one(db) @@ -51,7 +51,7 @@ impl Claims { } pub async fn new(email: String, password: String, db: &PgPool) -> Option<Self> { let user = sqlx::query!( - r#"SELECT id, role AS "role: Role", password_hash, university FROM participants WHERE email = $1"#, + r#"SELECT id, role AS "role: Role", password_hash, university AS "university: University" FROM participants WHERE email = $1"#, email ) .fetch_one(db) diff --git a/server/src/bin/create_admin.rs b/server/src/bin/create_admin.rs index a878bf8..e421db2 100644 --- a/server/src/bin/create_admin.rs +++ b/server/src/bin/create_admin.rs @@ -1,5 +1,8 @@ use backend_cqi::{ - model::{competition::Competition, minimal_participant::MinimalParticipant, role::Role}, + model::{ + competition::Competition, minimal_participant::MinimalParticipant, role::Role, + university::University, + }, Result, }; use rand::distributions::{Alphanumeric, DistString}; @@ -18,10 +21,9 @@ async fn main() -> Result<()> { email: "mamanningham@cqi-qec.qc.ca".to_string(), competition: Competition::None, role: Role::Organizer, + university: University::Polymtl, }; - participant - .write_to_database(&password, &db, "".to_string()) - .await?; + participant.write_to_database(&password, &db).await?; println!("Password: {}", password); Ok(()) } diff --git a/server/src/model/minimal_participant.rs b/server/src/model/minimal_participant.rs index 1461713..e96af40 100644 --- a/server/src/model/minimal_participant.rs +++ b/server/src/model/minimal_participant.rs @@ -5,7 +5,7 @@ use sqlx::PgPool; use ts_rs::TS; use uuid::Uuid; -use super::{competition::Competition, role::Role}; +use super::{competition::Competition, role::Role, university::University}; #[derive(Debug, Serialize, Deserialize, TS, sqlx::FromRow)] #[ts(export)] @@ -14,6 +14,7 @@ pub struct MinimalParticipant { pub last_name: String, pub email: String, pub competition: Competition, + pub university: University, pub role: Role, } @@ -24,12 +25,7 @@ impl MinimalParticipant { .await } - pub async fn write_to_database( - &self, - password: &str, - db: &PgPool, - university: String, - ) -> Result<(), sqlx::Error> { + pub async fn write_to_database(&self, password: &str, db: &PgPool) -> Result<(), sqlx::Error> { let id = Uuid::new_v4(); tracing::info!("Generated password: {}", password); // TODO: remove this debug line let salt = SaltString::generate(&mut OsRng); @@ -48,7 +44,7 @@ impl MinimalParticipant { self.first_name, self.last_name, self.competition as Competition, - university + self.university as University ) .execute(db) .await?; diff --git a/server/src/model/mod.rs b/server/src/model/mod.rs index 5c6b6a9..9ad9d7e 100644 --- a/server/src/model/mod.rs +++ b/server/src/model/mod.rs @@ -3,4 +3,4 @@ pub mod minimal_participant; pub mod participant; pub mod preview_participant; pub mod role; -pub mod staff; +pub mod university; diff --git a/server/src/model/participant.rs b/server/src/model/participant.rs index c6d9aa8..4194a84 100644 --- a/server/src/model/participant.rs +++ b/server/src/model/participant.rs @@ -5,7 +5,7 @@ use sqlx::PgPool; use ts_rs::TS; use uuid::Uuid; -use super::{competition::Competition, role::Role}; +use super::{competition::Competition, role::Role, university::University}; use crate::utility::{deserialize_base64, serialize_base64}; #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, TS)] @@ -16,7 +16,7 @@ pub struct Participant { pub email: String, pub first_name: String, pub last_name: String, - pub university: String, + pub university: University, pub medical_conditions: Option<String>, pub allergies: Option<String>, pub pronouns: Option<String>, @@ -49,9 +49,21 @@ pub struct Participant { } impl Participant { - pub async fn get_participant(db: &PgPool) -> Result<Self, sqlx::Error> { + pub async fn get_participant(id: Uuid, db: &PgPool) -> Result<Self, sqlx::Error> { sqlx::query_as("SELECT * FROM participants WHERE id = $1") - .bind(Uuid::new_v4()) + .bind(id) + .fetch_one(db) + .await + } + + pub async fn get_participant_with_university( + id: Uuid, + university: University, + db: &PgPool, + ) -> Result<Self, sqlx::Error> { + sqlx::query_as("SELECT * FROM participants WHERE id = $1 AND university = $2") + .bind(id) + .bind(university) .fetch_one(db) .await } @@ -90,13 +102,13 @@ impl Participant { pub async fn delete_from_database_university( id: Uuid, - university: String, + university: University, db: &PgPool, ) -> Result<(), sqlx::Error> { sqlx::query!( r#"DELETE FROM participants WHERE id = $1 AND university = $2"#, id, - university + university as University ) .execute(db) .await?; diff --git a/server/src/model/preview_participant.rs b/server/src/model/preview_participant.rs index c4f67dd..693d355 100644 --- a/server/src/model/preview_participant.rs +++ b/server/src/model/preview_participant.rs @@ -3,7 +3,7 @@ use sqlx::PgPool; use ts_rs::TS; use uuid::Uuid; -use super::{competition::Competition, role::Role}; +use super::{competition::Competition, role::Role, university::University}; #[derive(Debug, Serialize, Deserialize, TS, sqlx::FromRow)] #[ts(export)] @@ -28,7 +28,7 @@ impl ParticipantPreview { pub async fn get_participants_from_university( db: &PgPool, - university: &str, + university: University, ) -> Result<Vec<Self>, sqlx::Error> { sqlx::query_as("SELECT * FROM participants WHERE university_name = $1") .bind(university) diff --git a/server/src/model/staff.rs b/server/src/model/staff.rs deleted file mode 100644 index e69de29..0000000 diff --git a/server/src/model/university.rs b/server/src/model/university.rs new file mode 100644 index 0000000..882aaf1 --- /dev/null +++ b/server/src/model/university.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, sqlx::Type, TS)] +#[serde(rename_all = "snake_case")] +#[sqlx(rename_all = "snake_case", type_name = "UNIVERSITY")] +#[ts(export)] +pub enum University { + Uqac, + Uqar, + Uqat, + Uqo, + Uqtr, + Mcgill, + McgillMacdonald, + Concordia, + Ets, + Polymtl, + Ulaval, + UlavalAgriculture, + Uds, + None, +} diff --git a/server/src/routes/get_participant.rs b/server/src/routes/get_participant.rs index e69de29..de61cc9 100644 --- a/server/src/routes/get_participant.rs +++ b/server/src/routes/get_participant.rs @@ -0,0 +1,50 @@ +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::Json; +use uuid::Uuid; + +use crate::model::participant::Participant; +use crate::{auth::claims::Claims, model::role::Role, SharedState}; + +pub async fn get_participant( + claims: Claims, + State(state): State<SharedState>, + Path(id): Path<Uuid>, +) -> impl IntoResponse { + match claims.role { + Role::Organizer | Role::Volunteer => { + match Participant::get_participant(id, &state.db).await { + Ok(participant) => (StatusCode::OK, Json(participant)).into_response(), + Err(e) => { + tracing::error!("Failed to get participant: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + } + } + } + Role::Chef => { + match Participant::get_participant_with_university(id, claims.university, &state.db) + .await + { + Ok(participant) => (StatusCode::OK, Json(participant)).into_response(), + Err(e) => { + tracing::error!("Failed to get participant: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + } + } + } + Role::Participant => { + if id == claims.id { + match Participant::get_participant(id, &state.db).await { + Ok(participant) => (StatusCode::OK, Json(participant)).into_response(), + Err(e) => { + tracing::error!("Failed to get participant: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + } + } + } else { + (StatusCode::FORBIDDEN, "Forbidden").into_response() + } + } + } +} diff --git a/server/src/routes/get_participants.rs b/server/src/routes/get_participants.rs index 4240c5e..4011c65 100644 --- a/server/src/routes/get_participants.rs +++ b/server/src/routes/get_participants.rs @@ -21,11 +21,8 @@ pub async fn get_participants( } } Role::Chef => { - match ParticipantPreview::get_participants_from_university( - &state.db, - &claims.university, - ) - .await + match ParticipantPreview::get_participants_from_university(&state.db, claims.university) + .await { Ok(participants) => return (StatusCode::OK, Json(participants)).into_response(), Err(e) => { diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index dc51889..222dd6c 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -8,6 +8,7 @@ use crate::SharedState; pub mod change_password; pub mod delete_participant; +pub mod get_participant; pub mod get_participants; pub mod login; pub mod new_participant; diff --git a/server/src/routes/new_participant.rs b/server/src/routes/new_participant.rs index a2e7531..8336ae9 100644 --- a/server/src/routes/new_participant.rs +++ b/server/src/routes/new_participant.rs @@ -4,12 +4,16 @@ use rand::distributions::{Alphanumeric, DistString}; use crate::{auth::claims::Claims, model::minimal_participant::MinimalParticipant, SharedState}; pub async fn new_participant( - _claims: Claims, + claims: Claims, State(state): State<SharedState>, Json(participant): Json<MinimalParticipant>, ) -> impl IntoResponse { let password = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + if participant.university != claims.university { + return (StatusCode::FORBIDDEN, "Forbidden").into_response(); + } + if let Err(e) = state .email .lock() @@ -48,10 +52,7 @@ https://cqi-qec.qc.ca/login return (StatusCode::BAD_REQUEST, e.to_string()).into_response(); }; - if let Err(e) = participant - .write_to_database(&password, &state.db, _claims.university) - .await - { + if let Err(e) = participant.write_to_database(&password, &state.db).await { return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(); }; -- GitLab