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