From af6d51d4953ca119d542c232056db22a945d1a03 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Manningham <marc-antoine.m@outlook.com> Date: Wed, 23 Oct 2024 15:28:11 -0400 Subject: [PATCH] feat: add password reset --- client/src/binding/EmailResetPayload.ts | 3 + client/src/binding/MinimalParticipant.ts | 2 + client/src/binding/Participant.ts | 7 ++- client/src/binding/University.ts | 17 +++++ client/src/i18n/en.ts | 22 +++++++ client/src/i18n/fr.ts | 22 +++++++ client/src/routes/AdditionalForm.tsx | 80 ++++-------------------- server/src/model/participant.rs | 10 +-- server/src/routes/mod.rs | 3 +- server/src/routes/send_email_reset.rs | 46 ++++++++++++++ 10 files changed, 135 insertions(+), 77 deletions(-) create mode 100644 client/src/binding/EmailResetPayload.ts create mode 100644 client/src/binding/University.ts create mode 100644 server/src/routes/send_email_reset.rs diff --git a/client/src/binding/EmailResetPayload.ts b/client/src/binding/EmailResetPayload.ts new file mode 100644 index 0000000..94648bd --- /dev/null +++ b/client/src/binding/EmailResetPayload.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 EmailResetPayload = { email: string }; diff --git a/client/src/binding/MinimalParticipant.ts b/client/src/binding/MinimalParticipant.ts index bb2e0d2..be03e09 100644 --- a/client/src/binding/MinimalParticipant.ts +++ b/client/src/binding/MinimalParticipant.ts @@ -1,11 +1,13 @@ // 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"; export type MinimalParticipant = { first_name: string; last_name: string; email: string; competition: Competition; + university: University; role: Role; }; diff --git a/client/src/binding/Participant.ts b/client/src/binding/Participant.ts index cf21abe..8beddae 100644 --- a/client/src/binding/Participant.ts +++ b/client/src/binding/Participant.ts @@ -1,6 +1,7 @@ // 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"; export type Participant = { id: string; @@ -8,11 +9,13 @@ export type Participant = { email: string; first_name: string; last_name: string; - university: string | null; + competition: Competition | null; + university: University; medical_conditions: string | null; allergies: string | null; pronouns: string | null; - competition: Competition | null; + supper: string | null; + is_vegetarian: boolean | null; phone_number: string | null; tshirt_size: string | null; comments: string | null; diff --git a/client/src/binding/University.ts b/client/src/binding/University.ts new file mode 100644 index 0000000..f62004e --- /dev/null +++ b/client/src/binding/University.ts @@ -0,0 +1,17 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type University = + | "uqac" + | "uqar" + | "uqat" + | "uqo" + | "uqtr" + | "mcgill" + | "mcgill_macdonald" + | "concordia" + | "ets" + | "polymtl" + | "ulaval" + | "ulaval_agriculture" + | "uds" + | "none"; diff --git a/client/src/i18n/en.ts b/client/src/i18n/en.ts index 5678054..0e871e8 100644 --- a/client/src/i18n/en.ts +++ b/client/src/i18n/en.ts @@ -98,6 +98,27 @@ const loginPage = { signIn: "Sign in", } +const additionalInfo = { + medicalConditions: "Medical conditions", + medicalConditionsLabel: + "Please specify any medical conditions that we should be aware of.", + allergies: "Allergies", + allergiesLabel: "Please specify any allergies that we should be aware of.", + pronouns: "Pronouns", + pronounsLabel: "Please specify your pronouns.", + vegetarianLabel: "Are you vegetarian?", + vegetarian: "Vegetarian", + phoneNumber: "Phone number", + phoneNumberLabel: "Please specify your phone number.", + emergencyContact: "Emergency contact", + emergencyContactLabel: "Please specify your emergency contact.", + hasMonthlyOpusCard: "Do you have a monthly OPUS card?", + reducedMobility: "Any acommodation for reduced mobility?", + studyProof: "Study proof", + photo: "Photo", + cv: "CV", +} + export const dict = { cqi: "QEC", lang: "EN", @@ -109,5 +130,6 @@ export const dict = { documents: documents, login: "Login", loginPage: loginPage, + additionalInfo: additionalInfo, madeBy: "Made by", } diff --git a/client/src/i18n/fr.ts b/client/src/i18n/fr.ts index dff81fc..ea6c0f9 100644 --- a/client/src/i18n/fr.ts +++ b/client/src/i18n/fr.ts @@ -98,6 +98,27 @@ const loginPage = { signIn: "Se connecter", } +const additionalInfo = { + medicalConditions: "Medical conditions", + medicalConditionsLabel: + "Please specify any medical conditions that we should be aware of.", + allergies: "Allergies", + allergiesLabel: "Please specify any allergies that we should be aware of.", + pronouns: "Pronouns", + pronounsLabel: "Please specify your pronouns.", + vegetarianLabel: "Are you vegetarian?", + vegetarian: "Vegetarian", + phoneNumber: "Phone number", + phoneNumberLabel: "Please specify your phone number.", + emergencyContact: "Emergency contact", + emergencyContactLabel: "Please specify your emergency contact.", + hasMonthlyOpusCard: "Do you have a monthly OPUS card?", + reducedMobility: "Any acommodation for reduced mobility?", + studyProof: "Study proof", + photo: "Photo", + cv: "CV", +} + export const dict = { cqi: "CQI", lang: "FR", @@ -110,4 +131,5 @@ export const dict = { login: "Connexion", madeBy: "Créé par", loginPage: loginPage, + additionalInfo: additionalInfo, } diff --git a/client/src/routes/AdditionalForm.tsx b/client/src/routes/AdditionalForm.tsx index 16c04e3..37b0027 100644 --- a/client/src/routes/AdditionalForm.tsx +++ b/client/src/routes/AdditionalForm.tsx @@ -3,6 +3,7 @@ import FixedImage from "../components/FixedImage" import { useNavigate } from "@solidjs/router" import { ProtectedRoute } from "../components/ProtectedRoute" import { Participant } from "../binding/Participant" +import { TextInput } from "../components/forms-component/TextInput" export default function AdditionalForm() { const navigate = useNavigate() @@ -11,39 +12,7 @@ export default function AdditionalForm() { const handleSubmit: SubmitHandler<Participant> = (values, event) => { event.preventDefault() console.log(values) - navigate("/leader") - } - - interface Props { - id: any - name: string - } - - function TextField(prop: Props) { - return ( - <Field name={prop.id}> - {(_field, props) => ( - <div> - <label - for={prop.id} - class="block text-sm font-medium leading-6 text-gray-900" - > - {prop.name} - </label> - <div class="mt-2"> - <input - {...props} - id={prop.id} - name={prop.id} - type="text" - required - class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-light-highlight sm:text-sm sm:leading-6" - /> - </div> - </div> - )} - </Field> - ) + navigate("/dashboard") } return ( @@ -61,48 +30,21 @@ export default function AdditionalForm() { method="post" onSubmit={handleSubmit} > - <TextField - id="medical_conditions" - name="Conditions médicales" - /> - <TextField id="allergies" name="Allergies" /> - <TextField id="pronouns" name="Pronoms" /> - <TextField - id="phone_number" - name="Numéro de téléphone" - /> - <TextField id="tshirt_size" name="Taille de T-shirt" /> - <TextField id="comments" name="Commentaires" /> - <TextField - id="emergency_contact" - name="Contact d'urgence" - /> - - <Field name="has_monthly_opus_card"> - {(_field, props) => ( - <div class="flex items-center"> - <input - {...props} - id="has_monthly_opus_card" - type="checkbox" - value="" - class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600" - /> - <label - for="has_monthly_opus_card" - class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300" - > - Je possède un abonnement pour la carte - OPUS. - </label> - </div> + <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 text-sm 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" + 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> diff --git a/server/src/model/participant.rs b/server/src/model/participant.rs index 4194a84..1cab8ce 100644 --- a/server/src/model/participant.rs +++ b/server/src/model/participant.rs @@ -16,12 +16,13 @@ pub struct Participant { pub email: String, pub first_name: String, pub last_name: String, + pub competition: Competition, pub university: University, pub medical_conditions: Option<String>, pub allergies: Option<String>, pub pronouns: Option<String>, pub supper: Option<String>, - pub competition: Option<Competition>, + pub is_vegetarian: Option<bool>, pub phone_number: Option<String>, pub tshirt_size: Option<String>, pub comments: Option<String>, @@ -70,13 +71,12 @@ impl Participant { pub async fn write_to_database(&self, db: &PgPool) -> Result<(), sqlx::Error> { sqlx::query!( - r#"UPDATE participants SET (medical_conditions, allergies, supper, pronouns, competition, 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, $14) WHERE id = $15"#, + 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"#, self.medical_conditions, self.allergies, - self.supper, + self.is_vegetarian, self.pronouns, - self.competition as Option<Competition>, self.phone_number, self.tshirt_size, self.comments, diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 222dd6c..c6714a4 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -13,6 +13,7 @@ pub mod get_participants; pub mod login; pub mod new_participant; pub mod patch_participant; +pub mod send_email_reset; pub mod test_token; pub fn api_router(state: SharedState) -> Router { @@ -29,6 +30,6 @@ pub fn api_router(state: SharedState) -> Router { .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)) + .route("/password", put(change_password::change_password).post(send_email_reset::send_email_reset)) .with_state(state) } diff --git a/server/src/routes/send_email_reset.rs b/server/src/routes/send_email_reset.rs new file mode 100644 index 0000000..607baf0 --- /dev/null +++ b/server/src/routes/send_email_reset.rs @@ -0,0 +1,46 @@ +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use serde::Deserialize; +use ts_rs::TS; + +use crate::{auth::claims::Claims, SharedState}; + +#[derive(Deserialize, TS)] +#[ts(export)] +pub struct EmailResetPayload { + email: String, +} + +pub async fn send_email_reset( + State(state): State<SharedState>, + Json(email): Json<EmailResetPayload>, +) -> impl IntoResponse { + let Some(claims) = + Claims::create_token_for_password_reset(email.email.clone(), &state.db).await + else { + return (StatusCode::BAD_REQUEST, "Email not found".to_string()).into_response(); + }; + if let Err(e) = state + .email + .lock() + .await + .send_email( + "Changement de mot de passe CQI/QEC 2025 password reinitalization", + format!( + r#"Bonjour, +Un changement de mot de passe a été demandé pour le compte associé à ce courriel. +Pour changer votre mot de passe, cliquez sur le lien suivant : + +A password change has been requested for the account associated with this email. +To change your password, click on the following link: + +https://cqi-qec.qc.ca/change-password/{}"#, + claims + ), + &email.email, + ) + .await + { + return (StatusCode::BAD_REQUEST, e.to_string()).into_response(); + }; + (StatusCode::OK, "Email sent".to_string()).into_response() +} -- GitLab