From a5bc84469c5d8a9ac42bed6c5e31655b75efbb8c Mon Sep 17 00:00:00 2001 From: Marc-Antoine Manningham <marc-antoine.m@outlook.com> Date: Wed, 23 Oct 2024 21:45:14 -0400 Subject: [PATCH] feat: good dialog for additional info --- TODO.md | 1 + client/src/binding/Claims.ts | 10 + .../{Participant.ts => ParticipantInfo.ts} | 21 +- client/src/binding/TshirtSize.ts | 3 + client/src/components/ProtectedRoute.tsx | 3 + .../components/forms-component/Checkbox.tsx | 1 + .../components/forms-component/FileInput.tsx | 2 +- .../components/forms/AdditionnalInfoForm.tsx | 239 ++++++++++++++++++ .../src/components/forms/NewPasswordForm.tsx | 8 +- .../src/components/forms/ParticipantForm.tsx | 6 +- client/src/request/fetch_wrapper.ts | 1 + client/src/request/routes.ts | 55 ++++ client/src/routes/AdditionalForm.tsx | 43 +--- ...2f3c4918a7eb20626e5827dd2ebd0b5c65294.json | 16 +- ...241015182258_create_participants_table.sql | 4 +- server/src/auth/claims.rs | 4 +- server/src/model/mod.rs | 3 +- .../{participant.rs => participant_info.rs} | 30 +-- server/src/model/tshirt_size.rs | 15 ++ server/src/routes/change_password.rs | 4 +- server/src/routes/delete_participant.rs | 12 +- server/src/routes/get_participant.rs | 8 +- server/src/routes/mod.rs | 7 +- server/src/routes/patch_participant.rs | 7 +- server/src/routes/test_token.rs | 11 +- 25 files changed, 407 insertions(+), 107 deletions(-) create mode 100644 client/src/binding/Claims.ts rename client/src/binding/{Participant.ts => ParticipantInfo.ts} (52%) create mode 100644 client/src/binding/TshirtSize.ts rename server/src/model/{participant.rs => participant_info.rs} (85%) create mode 100644 server/src/model/tshirt_size.rs diff --git a/TODO.md b/TODO.md index 06d42a1..3cdfdc9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ - Implement detailed view of Participant information - Client side field validation +- Fix Chef seeing CO and volunteer in same university - Traduction - Search functionality diff --git a/client/src/binding/Claims.ts b/client/src/binding/Claims.ts new file mode 100644 index 0000000..0f6a7da --- /dev/null +++ b/client/src/binding/Claims.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Role } from "./Role"; +import type { University } from "./University"; + +export type Claims = { + id: string; + role: Role; + university: University; + exp: number; +}; diff --git a/client/src/binding/Participant.ts b/client/src/binding/ParticipantInfo.ts similarity index 52% rename from client/src/binding/Participant.ts rename to client/src/binding/ParticipantInfo.ts index 8beddae..4d2a811 100644 --- a/client/src/binding/Participant.ts +++ b/client/src/binding/ParticipantInfo.ts @@ -1,28 +1,19 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Competition } from "./Competition"; -import type { Role } from "./Role"; -import type { University } from "./University"; +import type { TshirtSize } from "./TshirtSize"; -export type Participant = { - id: string; - role: Role; - email: string; - first_name: string; - last_name: string; - competition: Competition | null; - university: University; +export type ParticipantInfo = { medical_conditions: string | null; allergies: string | null; pronouns: string | null; supper: string | null; is_vegetarian: boolean | null; phone_number: string | null; - tshirt_size: string | null; + tshirt_size: TshirtSize | null; comments: string | null; emergency_contact: string | null; has_monthly_opus_card: boolean | null; reduced_mobility: string | null; - study_proof: string; - photo: string; - cv: string; + study_proof: File; + photo: File; + cv: File; }; diff --git a/client/src/binding/TshirtSize.ts b/client/src/binding/TshirtSize.ts new file mode 100644 index 0000000..0125638 --- /dev/null +++ b/client/src/binding/TshirtSize.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TshirtSize = "xs" | "s" | "m" | "l" | "xl" | "xxl"; diff --git a/client/src/components/ProtectedRoute.tsx b/client/src/components/ProtectedRoute.tsx index 95d5749..dc07511 100644 --- a/client/src/components/ProtectedRoute.tsx +++ b/client/src/components/ProtectedRoute.tsx @@ -17,6 +17,9 @@ export function ProtectedRoute(props: ProtectedRouteProps) { localStorage.removeItem("token") navigate("/login") } + localStorage.setItem("id", response.id) + localStorage.setItem("role", response.role) + localStorage.setItem("university", response.university) }) return <>{props.children}</> diff --git a/client/src/components/forms-component/Checkbox.tsx b/client/src/components/forms-component/Checkbox.tsx index fb9b1ab..4da6bfb 100644 --- a/client/src/components/forms-component/Checkbox.tsx +++ b/client/src/components/forms-component/Checkbox.tsx @@ -1,5 +1,6 @@ import { JSX, splitProps } from "solid-js" import { InputError } from "./InputError" +import clsx from "clsx" type CheckboxProps = { ref: (element: HTMLInputElement) => void diff --git a/client/src/components/forms-component/FileInput.tsx b/client/src/components/forms-component/FileInput.tsx index 41999ef..b41f9ca 100644 --- a/client/src/components/forms-component/FileInput.tsx +++ b/client/src/components/forms-component/FileInput.tsx @@ -42,7 +42,7 @@ export function FileInput(props: FileInputProps) { ) return ( - <div class={clsx("px-8 lg:px-10", props.class)}> + <div> <InputLabel name={props.name} label={props.label} diff --git a/client/src/components/forms/AdditionnalInfoForm.tsx b/client/src/components/forms/AdditionnalInfoForm.tsx index e69de29..da7c930 100644 --- a/client/src/components/forms/AdditionnalInfoForm.tsx +++ b/client/src/components/forms/AdditionnalInfoForm.tsx @@ -0,0 +1,239 @@ +import { useNavigate } from "@solidjs/router" +import { createForm, setValues, SubmitHandler } from "@modular-forms/solid" +import { TextInput } from "../forms-component/TextInput" +import { Checkbox } from "../forms-component/Checkbox" +import { ParticipantInfo } from "../../binding/ParticipantInfo" +import { Select } from "../forms-component/Select" +import { FileInput } from "../forms-component/FileInput" +import { getParticipant, patchParticipantInfo } from "../../request/routes" +import { createEffect } from "solid-js" + +export function AdditionalInfoForm() { + const navigate = useNavigate() + const [loginForm, { Form, Field }] = createForm<ParticipantInfo>() + + const handleSubmit: SubmitHandler<ParticipantInfo> = async ( + values, + event, + ) => { + event.preventDefault() + const response = await patchParticipantInfo(values) + if (response && response.status == 201) { + navigate("/dashboard") + } else { + console.log(response) + } + } + + createEffect(async () => { + const id = localStorage.getItem("id") + if (!id) return + const response = await getParticipant(id) + if (!response.error) { + const participant = response as ParticipantInfo + setValues(loginForm, participant) + } + }) + + return ( + <Form + class="space-y-6" + action="#" + method="post" + onSubmit={handleSubmit} + > + <Field name="medical_conditions"> + {(field, props) => ( + <TextInput + {...props} + value={field.value || ""} + error={field.error} + label="Conditions médicales" + type="text" + placeholder="Conditions médicales" + /> + )} + </Field> + + <Field name="allergies"> + {(field, props) => ( + <TextInput + {...props} + value={field.value || ""} + error={field.error} + label="Allergies" + type="text" + placeholder="Allergies" + /> + )} + </Field> + + <Field name="pronouns"> + {(field, props) => ( + <TextInput + {...props} + value={field.value || ""} + error={field.error} + label="Pronoms" + type="text" + placeholder="Pronoms" + /> + )} + </Field> + + <Field name="is_vegetarian" type="boolean"> + {(field, props) => ( + <Checkbox + {...props} + checked={field.value || false} + error={field.error} + label="Végétarien(ne)" + /> + )} + </Field> + + <Field name="phone_number"> + {(field, props) => ( + <TextInput + {...props} + value={field.value || ""} + error={field.error} + label="Numéro de téléphone" + type="text" + placeholder="Numéro de téléphone" + /> + )} + </Field> + + <Field name="tshirt_size"> + {(field, props) => ( + <Select + {...props} + value={field.value || "m"} + error={field.error} + options={[ + { + label: "XS", + value: "xs", + }, + { + label: "S", + value: "s", + }, + { + label: "M", + value: "m", + }, + { + label: "L", + value: "l", + }, + { + label: "XL", + value: "xl", + }, + { + label: "2XL", + value: "xxl", + }, + ]} + /> + )} + </Field> + + <Field name="comments"> + {(field, props) => ( + <TextInput + {...props} + value={field.value || ""} + error={field.error} + label="Commentaires" + type="text" + placeholder="Commentaires" + /> + )} + </Field> + + <Field name="emergency_contact"> + {(field, props) => ( + <TextInput + {...props} + value={field.value || ""} + error={field.error} + label="Contact d'urgence" + type="text" + placeholder="Contact d'urgence" + /> + )} + </Field> + + <Field name="has_monthly_opus_card" type="boolean"> + {(field, props) => ( + <Checkbox + {...props} + error={field.error} + label="Carte OPUS mensuelle" + /> + )} + </Field> + + <Field name="reduced_mobility"> + {(field, props) => ( + <TextInput + {...props} + value={field.value || ""} + error={field.error} + label="Mobilité réduite" + type="text" + placeholder="Commentaires" + /> + )} + </Field> + + <Field name="study_proof" type="File"> + {(field, props) => ( + <FileInput + {...props} + value={field.value} + error={field.error} + label="Preuve d'études" + accept="image/*,.pdf,.doc,.docx,.odt,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document" + /> + )} + </Field> + + <Field name="photo" type="File"> + {(field, props) => ( + <FileInput + {...props} + value={field.value} + error={field.error} + label="Photo" + accept="image/*" + /> + )} + </Field> + + <Field name="cv" type="File"> + {(field, props) => ( + <FileInput + {...props} + value={field.value} + error={field.error} + label="CV" + accept=".pdf,.doc,.docx,.odt,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document" + /> + )} + </Field> + + <div> + <button + type="submit" + class="flex w-full justify-center rounded-md bg-light-highlight px-3 py-1.5 text-xl font-semibold leading-6 text-white shadow-sm hover:bg-light-highlight focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-light-highlight" + > + Mettre à jour les renseignements personnels + </button> + </div> + </Form> + ) +} diff --git a/client/src/components/forms/NewPasswordForm.tsx b/client/src/components/forms/NewPasswordForm.tsx index e38a66c..a3849ab 100644 --- a/client/src/components/forms/NewPasswordForm.tsx +++ b/client/src/components/forms/NewPasswordForm.tsx @@ -3,15 +3,19 @@ import { ChangePasswordPayload } from "../../binding/ChangePasswordPayload" import { changePassword } from "../../request/routes" import { TextInput } from "../forms-component/TextInput" import { SubmitError } from "../forms-component/SubmitError" -import { createSignal } from "solid-js" +import { createEffect, createSignal } from "solid-js" export function NewPassword() { const [_form, { Form, Field }] = createForm<ChangePasswordPayload>() const [error, setError] = createSignal<string | null>(null) const onSubmit = async (data: ChangePasswordPayload) => { - await changePassword(data) + const response = await changePassword(data) + if (response.error) { + setError("Erreur lors du changement de mot de passe") + } } + createEffect(async () => {}) return ( <Form onSubmit={onSubmit} class="flex flex-col gap-8"> <Field name="new_password"> diff --git a/client/src/components/forms/ParticipantForm.tsx b/client/src/components/forms/ParticipantForm.tsx index 7baa521..8c0d26a 100644 --- a/client/src/components/forms/ParticipantForm.tsx +++ b/client/src/components/forms/ParticipantForm.tsx @@ -1,4 +1,4 @@ -import { Info, PlusCircle, Trash } from "phosphor-solid-js" +import { PlusCircle, Trash } from "phosphor-solid-js" import { MinimalParticipant } from "../../binding/MinimalParticipant" import { createResource, For } from "solid-js" import { createForm } from "@modular-forms/solid" @@ -30,12 +30,12 @@ function ParticipantRow(props: ParticipantRowProps) { <td class="p-2 text-center">{p.competition}</td> <td class="p-2 text-center">{p.role}</td> <td class="flex flex-row gap-4 p-2 text-center"> - <button + {/* <button type="button" class="rounded bg-blue-500 p-1 font-bold text-white hover:bg-blue-700" > <Info class="h-8 w-8"></Info> - </button> + </button> */} <button type="button" class="rounded bg-red-500 p-1 font-bold text-white hover:bg-red-700" diff --git a/client/src/request/fetch_wrapper.ts b/client/src/request/fetch_wrapper.ts index ed6e17c..58a6e1d 100644 --- a/client/src/request/fetch_wrapper.ts +++ b/client/src/request/fetch_wrapper.ts @@ -72,6 +72,7 @@ export async function fetch_patch(url: string, body: any) { return await fetch(import.meta.env.VITE_API_URL + url, { method: "PATCH", headers: { + "Content-Type": "application/json", Authorization: "Bearer " + token, }, body: JSON.stringify(body), diff --git a/client/src/request/routes.ts b/client/src/request/routes.ts index 9f4f91f..73cce4c 100644 --- a/client/src/request/routes.ts +++ b/client/src/request/routes.ts @@ -2,10 +2,12 @@ import { AuthPayload } from "../binding/AuthPayload" import { ChangePasswordPayload } from "../binding/ChangePasswordPayload" import { EmailResetPayload } from "../binding/EmailResetPayload" import { MinimalParticipant } from "../binding/MinimalParticipant" +import { ParticipantInfo } from "../binding/ParticipantInfo" import { ParticipantPreview } from "../binding/ParticipantPreview" import { fetch_delete, fetch_get, + fetch_patch, fetch_post, fetch_post_no_token, fetch_put, @@ -54,3 +56,56 @@ export async function testAuth() { export async function resetEmail(email: EmailResetPayload) { return await fetch_post_no_token("/password", email) } + +function getBase64(file: File): Promise<string> { + return new Promise((resolve, reject) => { + const reader = new FileReader() + + // Event listener when the file reading is completed + reader.onload = () => { + // `reader.result` contains the base64 string + const base64String = reader.result as string + const base64StringSplit = base64String.split(",")[1] + if (!base64StringSplit) { + reject(new Error("Failed to convert file to base64")) + } + resolve(base64StringSplit) // Removing the base64 prefix + } + + // Event listener for any error + reader.onerror = () => { + reject(new Error("Failed to convert file to base64")) + } + + // Reading the file as a data URL (which contains base64 encoded string) + reader.readAsDataURL(file) + }) +} + +export async function patchParticipantInfo(info: ParticipantInfo) { + const payload: any = info + if (info.study_proof) { + payload.study_proof = await getBase64(info.study_proof) + } else { + payload.study_proof = "" + } + if (info.photo) { + payload.photo = await getBase64(info.photo) + } else { + payload.photo = "" + } + if (info.cv) { + payload.cv = await getBase64(info.cv) + } else { + payload.cv = "" + } + return await fetch_patch("/participant", payload) +} + +export async function getParticipant(id: string) { + const response = await fetch_get("/participant/" + id) + if (!response) { + return undefined + } + return await response.json() +} diff --git a/client/src/routes/AdditionalForm.tsx b/client/src/routes/AdditionalForm.tsx index 37b0027..6b17a01 100644 --- a/client/src/routes/AdditionalForm.tsx +++ b/client/src/routes/AdditionalForm.tsx @@ -1,55 +1,18 @@ -import { createForm, SubmitHandler } from "@modular-forms/solid" import FixedImage from "../components/FixedImage" -import { useNavigate } from "@solidjs/router" +import { AdditionalInfoForm } from "../components/forms/AdditionnalInfoForm" import { ProtectedRoute } from "../components/ProtectedRoute" -import { Participant } from "../binding/Participant" -import { TextInput } from "../components/forms-component/TextInput" export default function AdditionalForm() { - const navigate = useNavigate() - const [_loginForm, { Form, Field }] = createForm<Participant>() - - const handleSubmit: SubmitHandler<Participant> = (values, event) => { - event.preventDefault() - console.log(values) - navigate("/dashboard") - } - return ( <ProtectedRoute> <div class="flex w-full flex-col items-center justify-center"> <FixedImage url="/banners/documents.svg" height="32rem"> <h1 class="text-center font-futur text-6xl text-white"> - {"Tableau de bord des chefs"} + {"Tableau de bord"} </h1> </FixedImage> <div class="-mt-32 sm:mx-auto sm:w-full sm:max-w-sm"> - <Form - class="space-y-6" - action="#" - method="post" - onSubmit={handleSubmit} - > - <Field name="medical_conditions"> - {(field, props) => ( - <TextInput - {...props} - value={field.value || ""} - error={field.error} - label="Conditions médicales" - /> - )} - </Field> - - <div> - <button - type="submit" - class="flex w-full justify-center rounded-md bg-light-highlight px-3 py-1.5 font-semibold leading-6 text-white shadow-sm hover:bg-light-highlight focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-light-highlight" - > - Envoyer les renseignements personnels - </button> - </div> - </Form> + <AdditionalInfoForm /> </div> </div> </ProtectedRoute> diff --git a/server/.sqlx/query-b2cf8ace57c4a960f64b49060612f3c4918a7eb20626e5827dd2ebd0b5c65294.json b/server/.sqlx/query-b2cf8ace57c4a960f64b49060612f3c4918a7eb20626e5827dd2ebd0b5c65294.json index ba5d2a4..746f3d3 100644 --- a/server/.sqlx/query-b2cf8ace57c4a960f64b49060612f3c4918a7eb20626e5827dd2ebd0b5c65294.json +++ b/server/.sqlx/query-b2cf8ace57c4a960f64b49060612f3c4918a7eb20626e5827dd2ebd0b5c65294.json @@ -10,7 +10,21 @@ "Bool", "Text", "Text", - "Text", + { + "Custom": { + "name": "tshirt_size", + "kind": { + "Enum": [ + "xs", + "s", + "m", + "l", + "xl", + "xxl" + ] + } + } + }, "Text", "Text", "Bool", diff --git a/server/migrations/20241015182258_create_participants_table.sql b/server/migrations/20241015182258_create_participants_table.sql index 50c68da..031bdb7 100644 --- a/server/migrations/20241015182258_create_participants_table.sql +++ b/server/migrations/20241015182258_create_participants_table.sql @@ -4,6 +4,8 @@ CREATE TYPE COMPETITION AS ENUM ('none', 'conception_senior', 'conception_junior CREATE TYPE UNIVERSITY AS ENUM ('uqac', 'uqar', 'uqat', 'uqo', 'uqtr', 'mcgill', 'mcgill_macdonald', 'concordia', 'ets', 'polymtl', 'ulaval', 'ulaval-agriculture', 'uds', 'none'); +CREATE TYPE TSHIRT_SIZE AS ENUM ('xs', 's', 'm', 'l', 'xl', 'xxl'); + CREATE TABLE participants ( id UUID PRIMARY KEY, role ROLE NOT NULL, @@ -19,7 +21,7 @@ CREATE TABLE participants ( is_vegetarian BOOLEAN, pronouns TEXT, phone_number TEXT, - tshirt_size TEXT, + tshirt_size TSHIRT_SIZE, comments TEXT, emergency_contact TEXT, has_monthly_opus_card BOOLEAN, diff --git a/server/src/auth/claims.rs b/server/src/auth/claims.rs index cad2df0..72dc091 100644 --- a/server/src/auth/claims.rs +++ b/server/src/auth/claims.rs @@ -9,13 +9,15 @@ use chrono::Utc; use jsonwebtoken::{decode, Validation}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use ts_rs::TS; use uuid::Uuid; use crate::KEYS; use super::auth_error::AuthError; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] pub struct Claims { pub id: Uuid, pub role: Role, diff --git a/server/src/model/mod.rs b/server/src/model/mod.rs index 9ad9d7e..babe456 100644 --- a/server/src/model/mod.rs +++ b/server/src/model/mod.rs @@ -1,6 +1,7 @@ pub mod competition; pub mod minimal_participant; -pub mod participant; +pub mod participant_info; pub mod preview_participant; pub mod role; +pub mod tshirt_size; pub mod university; diff --git a/server/src/model/participant.rs b/server/src/model/participant_info.rs similarity index 85% rename from server/src/model/participant.rs rename to server/src/model/participant_info.rs index 1cab8ce..834ee49 100644 --- a/server/src/model/participant.rs +++ b/server/src/model/participant_info.rs @@ -5,26 +5,20 @@ use sqlx::PgPool; use ts_rs::TS; use uuid::Uuid; -use super::{competition::Competition, role::Role, university::University}; use crate::utility::{deserialize_base64, serialize_base64}; +use super::{tshirt_size::TshirtSize, university::University}; + #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, TS)] #[ts(export)] -pub struct Participant { - pub id: Uuid, - pub role: Role, - pub email: String, - pub first_name: String, - pub last_name: String, - pub competition: Competition, - pub university: University, +pub struct ParticipantInfo { pub medical_conditions: Option<String>, pub allergies: Option<String>, pub pronouns: Option<String>, pub supper: Option<String>, pub is_vegetarian: Option<bool>, pub phone_number: Option<String>, - pub tshirt_size: Option<String>, + pub tshirt_size: Option<TshirtSize>, pub comments: Option<String>, pub emergency_contact: Option<String>, pub has_monthly_opus_card: Option<bool>, @@ -33,23 +27,23 @@ pub struct Participant { serialize_with = "serialize_base64", deserialize_with = "deserialize_base64" )] - #[ts(type = "string")] + #[ts(type = "File")] pub study_proof: Option<Vec<u8>>, #[serde( serialize_with = "serialize_base64", deserialize_with = "deserialize_base64" )] - #[ts(type = "string")] + #[ts(type = "File")] pub photo: Option<Vec<u8>>, #[serde( serialize_with = "serialize_base64", deserialize_with = "deserialize_base64" )] - #[ts(type = "string")] + #[ts(type = "File")] pub cv: Option<Vec<u8>>, } -impl Participant { +impl ParticipantInfo { pub async fn get_participant(id: Uuid, db: &PgPool) -> Result<Self, sqlx::Error> { sqlx::query_as("SELECT * FROM participants WHERE id = $1") .bind(id) @@ -69,7 +63,7 @@ impl Participant { .await } - pub async fn write_to_database(&self, db: &PgPool) -> Result<(), sqlx::Error> { + pub async fn write_to_database(&self, id: Uuid, db: &PgPool) -> Result<(), sqlx::Error> { sqlx::query!( r#"UPDATE participants SET (medical_conditions, allergies, is_vegetarian, pronouns, phone_number, tshirt_size, comments, emergency_contact, has_monthly_opus_card, reduced_mobility, study_proof, photo, cv) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) WHERE id = $14"#, @@ -78,7 +72,7 @@ impl Participant { self.is_vegetarian, self.pronouns, self.phone_number, - self.tshirt_size, + self.tshirt_size as Option<TshirtSize>, self.comments, self.emergency_contact, self.has_monthly_opus_card, @@ -86,7 +80,7 @@ impl Participant { self.study_proof, self.photo, self.cv, - self.id + id ) .execute(db) .await?; @@ -100,7 +94,7 @@ impl Participant { Ok(()) } - pub async fn delete_from_database_university( + pub async fn delete_from_database_with_university( id: Uuid, university: University, db: &PgPool, diff --git a/server/src/model/tshirt_size.rs b/server/src/model/tshirt_size.rs new file mode 100644 index 0000000..4aa1ec9 --- /dev/null +++ b/server/src/model/tshirt_size.rs @@ -0,0 +1,15 @@ +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 = "TSHIRT_SIZE")] +#[ts(export)] +pub enum TshirtSize { + Xs, + S, + M, + L, + Xl, + Xxl, +} diff --git a/server/src/routes/change_password.rs b/server/src/routes/change_password.rs index d51da88..6997315 100644 --- a/server/src/routes/change_password.rs +++ b/server/src/routes/change_password.rs @@ -6,7 +6,7 @@ use serde::Deserialize; use ts_rs::TS; use crate::auth::claims::Claims; -use crate::model::participant::Participant; +use crate::model::participant_info::ParticipantInfo; use crate::SharedState; #[derive(Deserialize, TS)] @@ -20,7 +20,7 @@ pub async fn change_password( State(state): State<SharedState>, Json(password): Json<ChangePasswordPayload>, ) -> impl IntoResponse { - match Participant::change_password(claims.id, password.new_password, &state.db).await { + match ParticipantInfo::change_password(claims.id, password.new_password, &state.db).await { Ok(_) => (StatusCode::OK, "Password changed".to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), } diff --git a/server/src/routes/delete_participant.rs b/server/src/routes/delete_participant.rs index d377925..b9d3256 100644 --- a/server/src/routes/delete_participant.rs +++ b/server/src/routes/delete_participant.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::{ auth::claims::Claims, - model::{participant::Participant, role::Role}, + model::{participant_info::ParticipantInfo, role::Role}, SharedState, }; @@ -17,13 +17,17 @@ pub async fn delete_participant( Path(id): Path<Uuid>, ) -> impl IntoResponse { match claims.role { - Role::Organizer => match Participant::delete_from_database(id, &state.db).await { + Role::Organizer => match ParticipantInfo::delete_from_database(id, &state.db).await { Ok(_) => (StatusCode::CREATED, "Participant deleted".to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), }, Role::Chef => { - match Participant::delete_from_database_university(id, claims.university, &state.db) - .await + match ParticipantInfo::delete_from_database_with_university( + id, + claims.university, + &state.db, + ) + .await { Ok(_) => (StatusCode::CREATED, "Participant deleted".to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), diff --git a/server/src/routes/get_participant.rs b/server/src/routes/get_participant.rs index de61cc9..d074f12 100644 --- a/server/src/routes/get_participant.rs +++ b/server/src/routes/get_participant.rs @@ -4,7 +4,7 @@ use axum::response::IntoResponse; use axum::Json; use uuid::Uuid; -use crate::model::participant::Participant; +use crate::model::participant_info::ParticipantInfo; use crate::{auth::claims::Claims, model::role::Role, SharedState}; pub async fn get_participant( @@ -14,7 +14,7 @@ pub async fn get_participant( ) -> impl IntoResponse { match claims.role { Role::Organizer | Role::Volunteer => { - match Participant::get_participant(id, &state.db).await { + match ParticipantInfo::get_participant(id, &state.db).await { Ok(participant) => (StatusCode::OK, Json(participant)).into_response(), Err(e) => { tracing::error!("Failed to get participant: {}", e); @@ -23,7 +23,7 @@ pub async fn get_participant( } } Role::Chef => { - match Participant::get_participant_with_university(id, claims.university, &state.db) + match ParticipantInfo::get_participant_with_university(id, claims.university, &state.db) .await { Ok(participant) => (StatusCode::OK, Json(participant)).into_response(), @@ -35,7 +35,7 @@ pub async fn get_participant( } Role::Participant => { if id == claims.id { - match Participant::get_participant(id, &state.db).await { + match ParticipantInfo::get_participant(id, &state.db).await { Ok(participant) => (StatusCode::OK, Json(participant)).into_response(), Err(e) => { tracing::error!("Failed to get participant: {}", e); diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index c6714a4..7b81b8f 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -25,11 +25,14 @@ pub fn api_router(state: SharedState) -> Router { ) .route( "/participant/:id", - delete(delete_participant::delete_participant), + delete(delete_participant::delete_participant).get(get_participant::get_participant), ) .route("/participants", get(get_participants::get_participants)) .route("/login", post(login::login)) .route("/test", get(test_token::test_token)) - .route("/password", put(change_password::change_password).post(send_email_reset::send_email_reset)) + .route( + "/password", + put(change_password::change_password).post(send_email_reset::send_email_reset), + ) .with_state(state) } diff --git a/server/src/routes/patch_participant.rs b/server/src/routes/patch_participant.rs index 350063c..2c99769 100644 --- a/server/src/routes/patch_participant.rs +++ b/server/src/routes/patch_participant.rs @@ -1,12 +1,13 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; -use crate::{model::participant::Participant, SharedState}; +use crate::{auth::claims::Claims, model::participant_info::ParticipantInfo, SharedState}; pub async fn patch_participant( + claims: Claims, State(state): State<SharedState>, - Json(participant): Json<Participant>, + Json(participant): Json<ParticipantInfo>, ) -> impl IntoResponse { - match participant.write_to_database(&state.db).await { + match participant.write_to_database(claims.id, &state.db).await { Ok(_) => (StatusCode::CREATED, "Participant created".to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), } diff --git a/server/src/routes/test_token.rs b/server/src/routes/test_token.rs index 1c49ec6..684b706 100644 --- a/server/src/routes/test_token.rs +++ b/server/src/routes/test_token.rs @@ -1,14 +1,7 @@ use axum::{http::StatusCode, response::IntoResponse, Json}; -use serde::{Deserialize, Serialize}; -use crate::{auth::claims::Claims, model::role::Role}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuthResponse { - pub role: Role, -} +use crate::auth::claims::Claims; pub async fn test_token(claims: Claims) -> impl IntoResponse { - let auth = AuthResponse { role: claims.role }; - (StatusCode::OK, Json(auth)).into_response() + (StatusCode::OK, Json(claims)).into_response() } -- GitLab