diff --git a/Cargo.toml b/Cargo.toml index baca5b2..69bb813 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,9 @@ edition = "2021" [dependencies] axum = "0.7.5" +ldap3 = "0.11.3" pandoc = "0.8.11" -serde = { version = "1.0.197", features = ["derive"] } +serde = { version = "1.0.197", features = ["derive", "rc"] } sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] } strum = { version = "0.26.2", features = ["derive"] } tera = "1.19.1" @@ -17,3 +18,5 @@ tokio-util = { version = "0.7.10", features = ["io-util"] } tower-http = { version = "0.5.2", features = ["fs"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" +uuid = { version = "1.8.0", features = ["v5", "v4"] } +validator = { version = "0.17.0", features = ["derive"] } diff --git a/README.md b/README.md index ee919a4..8442b25 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,12 @@ La génération du PDF se fait juste après l'insertion des données dans la bas ## Todo fonctionnel Poc : -- [ ] formulaire de quelques champs, avec validation des données +- [x] formulaire de quelques champs, avec validation des données - [x] récupération de ces données et les injecter dans la base - [x] génération PDF du dossier -- [ ] transfert vers la page de paiement au besoin +- [x] lien vers le dossier pdf +- [x] transfert vers la page de paiement au besoin +- [ ] s'identifier via ldap, voir le logiciel du magasin pour la configuration - [ ] envoie de mail de confirmation Plus tard : diff --git a/src/main.rs b/src/main.rs index 188cf0b..17d3971 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,26 @@ -use std::path::PathBuf; +use std::{fmt::Display, path::PathBuf, sync::Arc}; -use axum::{extract::State, response::Html, routing::{get, post}, Form, Router}; +use axum::{extract::{Path, State}, http::{StatusCode, Uri}, response::{Html, Redirect}, routing::{get, post}, Form, Router}; +use ldap3::{LdapConn, LdapConnAsync, LdapConnSettings, Scope, SearchEntry}; use pandoc::{InputFormat, InputKind, MarkdownExtension, OutputKind}; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; -use strum::EnumIter; -use strum::IntoEnumIterator; +use strum::{EnumIter, IntoEnumIterator}; use tera::{Context, Tera}; +use tokio::sync::Mutex; use tower_http::services::ServeDir; use tracing::{debug, error, Level}; +use uuid::Uuid; +use validator::Validate; #[derive(Clone)] struct AppState { tera: Tera, - pool: SqlitePool + pool: SqlitePool, + // conserve les données saisies pour les représenter au cas où elles ne sont pas validées + form: Arc> } impl AppState { @@ -40,6 +45,7 @@ impl AppState { AppState { pool, tera, + form: Arc::new(Mutex::new(FormCandidate::new())) } } } @@ -55,10 +61,13 @@ async fn main() { // build our application with a single route let app = Router::new() .route("/", get(inscription)) + .route("/login", get(login)) + .route("/login", post(logged_in)) .route("/inscription", get(inscription)) - .route("/recapitulation", post(recapitulation)) + .route("/add_candidate", post(add_candidate)) + .route("/recapitulation/:uuid", get(recapitulation)) + .fallback(fallback) // Permet de servir les fichiers PDF des dossiers générés (static file) - // TODO: mettre une fallback .nest_service("/dossiers", ServeDir::new("./dossiers")) // .nest("dossiers", get(get_static_file)) .with_state(state); @@ -68,65 +77,214 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } -async fn inscription(State(state): State) -> Html { - let teracontext = tera::Context::new(); - Html(state.tera.render(Template::MainForm.display(), &teracontext).unwrap()) +async fn fallback(uri: Uri) -> (StatusCode, String) { + (StatusCode::NOT_FOUND, format!("No route for {uri}")) } -#[derive(Serialize, Deserialize, Debug, Clone, sqlx::FromRow)] -struct Candidate { +async fn inscription(State(state): State) -> Html { + debug!("Inscription"); + debug!("FormCandidate: {:?}", state.form); + let form = state.form.lock().await; + Html(state.tera.render(Template::Inscription.display(), &Context::from_serialize(form.clone()).unwrap()).unwrap()) +} + +async fn login(State(state): State) -> Html { + debug!("Login"); + Html(state.tera.render(Template::Login.display(), &Context::new()).unwrap()) +} + +#[derive(Serialize, Deserialize, Validate, Debug, Clone)] +struct Login { + login: String, + password: String, +} + +#[derive(Serialize, Deserialize, Validate, Debug, Clone, sqlx::FromRow)] +struct FormCandidate { + #[validate(length(min = 5))] #[sqlx(rename="prenom")] firstname: String, + #[validate(length(min = 5))] #[sqlx(rename="nom")] lastname: String, + birthdate: String, + birthplace: String, + birthplace_dep: String, + birthplace_country: String, + citizenship: String, + scholarship_holder: bool, + bea_number: String, + address: String, + postcode: String, + city: String, + country: String, + mobile_phone: String, + email: String, + year_n: Option, + year_n_minus_1: Option, + year_n_minus_2: Option, + year_n_minus_3: Option, + #[serde(skip_deserializing)] + errors: Vec, } -impl PartialEq for Candidate { + +impl FormCandidate { + fn new() -> FormCandidate { + FormCandidate { + firstname: String::new(), + lastname: String::new(), + birthdate: String::new(), + birthplace: String::new(), + birthplace_dep: String::new(), + birthplace_country: String::new(), + citizenship: String::new(), + scholarship_holder: false, + bea_number: String::new(), + errors: Vec::new(), + address: String::new(), + postcode: String::new(), + city: String::new(), + country: String::new(), + mobile_phone: String::new(), + email: String::new(), + year_n: None, + year_n_minus_1: None, + year_n_minus_2: None, + year_n_minus_3: None, + } + } +} + +impl PartialEq for FormCandidate { fn eq(&self, other: &Self) -> bool { self.firstname == other.firstname && self.lastname == other.lastname } } +#[derive(Clone,Serialize,Deserialize,Debug)] +enum YearOfBachelor { + First, + Second, + Third +} + +impl ToString for YearOfBachelor { + fn to_string(&self) -> String { + match self { + YearOfBachelor::First => String::from("1ère année"), + YearOfBachelor::Second => String::from("2ème année"), + YearOfBachelor::Third => String::from("3ème année"), + } + } +} + +#[derive(Clone,Serialize,Deserialize,Debug)] +struct AcademicYear { + degree: String, + year: YearOfBachelor, + school: String +} + +#[derive(Serialize, Deserialize, Validate, Debug, Clone, sqlx::FromRow)] +struct Candidate { + #[sqlx(rename="prenom")] + firstname: String, + #[sqlx(rename="nom")] + lastname: String, + #[sqlx(rename="uuid")] + uuid: String, +} + #[derive(EnumIter)] enum Template { - MainForm, + Dossier, + Inscription, + Login, Pdf } impl Template { fn display(&self) -> &str { match self { Template::Pdf => "pdf.html", - Template::MainForm => "index.html" + Template::Inscription => "inscription.html", + Template::Login => "login.html", + Template::Dossier => "dossier.html" } } } -async fn recapitulation(State(state): State, Form(candidate): Form) -> Html { - let inserted_candidate = db_add_candidate(&state, candidate).await.unwrap(); +async fn logged_in(State(state): State, Form(mut login): Form) -> Redirect { + debug!("Logged in"); + let (conn, mut ldap) = LdapConnAsync::with_settings(LdapConnSettings::new() + .set_starttls(false) + .set_no_tls_verify(false), + "ldaps://plg-dc2.ad.univ-lorraine.fr:636").await.unwrap(); + ldap3::drive!(conn); - debug!("PDF candidate: {:?}", inserted_candidate); + // Attempts a simple bind using the passed in values of username and Password + let result = ldap.simple_bind("fery3@univ-lorraine.fr", "gutsberserk48").await.unwrap().success(); + if result.is_ok() { + debug!("Auth OK"); + } else { + debug!("Auth Failed"); + } + // Ok(ldap.unbind()); + Redirect::to("/login") +} +async fn add_candidate(State(state): State, Form(mut candidate): Form) -> Redirect { + debug!("Add candidate"); + match candidate.validate() { + Ok(_) => { + let inserted_candidate = db_add_candidate(&state, candidate).await.unwrap(); - let rendered_data = state.tera.render(Template::Pdf.display(), &Context::from_serialize(&inserted_candidate).unwrap()).unwrap(); + debug!("PDF candidate: {:?}", inserted_candidate); - let mut pandoc = pandoc::new(); - pandoc.set_input(InputKind::Pipe(rendered_data)); - pandoc.set_input_format(InputFormat::Html,[MarkdownExtension::RawHtml].to_vec()); - pandoc.set_output(OutputKind::File(PathBuf::from("tmp.pdf"))); - pandoc.set_show_cmdline(true); - let _ = pandoc.execute().unwrap(); + let rendered_data = state.tera.render(Template::Pdf.display(), &Context::from_serialize(&inserted_candidate).unwrap()).unwrap(); - Html(state.tera.render("dossier.html", &Context::from_serialize(&inserted_candidate).unwrap()).unwrap()) + let mut pandoc = pandoc::new(); + pandoc.set_input(InputKind::Pipe(rendered_data)); + pandoc.set_input_format(InputFormat::Html,[MarkdownExtension::RawHtml].to_vec()); + pandoc.set_output(OutputKind::File(PathBuf::from(format!("/home/fery3/opt/projectc/dossiers/{}.pdf", inserted_candidate.uuid)))); + pandoc.set_show_cmdline(true); + let _ = pandoc.execute().unwrap(); + debug!("candidate validated"); + + Redirect::to(&format!("/recapitulation/{}",inserted_candidate.uuid)) + }, + Err(e) => { + for error in e.field_errors() { + let (field, _) = error; + candidate.errors.push(field.to_string()); + } + debug!("add candidate error array {:?}", candidate.errors); + + let mut form = state.form.lock().await; + *form = candidate.to_owned(); + // state.form = candidate; + debug!("candidate not validated {e}"); + Redirect::to("/") + } + } } -async fn db_add_candidate(state: &AppState, candidate: Candidate) -> Result{ +async fn recapitulation(State(state): State, Path(uuid): Path) -> Html { + + let candidate = db_get_candidate(&state, uuid); + Html(state.tera.render(Template::Dossier.display(), &Context::from_serialize(candidate.await.unwrap()).unwrap()).unwrap()) +} + +async fn db_add_candidate(state: &AppState, candidate: FormCandidate) -> Result{ debug!("db_add_candidate: {} {}", candidate.firstname, candidate.lastname); - let insert = sqlx::query("insert into users (prenom, nom) values ($1,$2)") + let uuid = Uuid::new_v4(); + let insert = sqlx::query("insert into users (prenom, nom, uuid) values ($1,$2,$3)") .bind(&candidate.firstname) .bind(&candidate.lastname) + .bind(uuid.to_string()) .execute(&state.pool).await; match insert { Ok(r) => { let userid = r.last_insert_rowid(); - sqlx::query_as::<_, Candidate>("select prenom, nom from users where id = $1") + sqlx::query_as::<_, Candidate>("select prenom, nom, uuid from users where id = $1") .bind(userid) .fetch_one(&state.pool).await } @@ -136,31 +294,43 @@ async fn db_add_candidate(state: &AppState, candidate: Candidate) -> Result Result{ + let candidate = sqlx::query_as::<_, Candidate>("select nom, prenom, uuid from users where uuid = $1") + .bind(token) + .fetch_one(&state.pool).await; + match candidate { + Ok(c) => Ok(c), + Err(e) => { + error!("add_candidate error: {}", e); + Err(e) + } + } } -#[tokio::test] -async fn test_template_list() { - let state = AppState::new().await; +// #[cfg(test)] +// #[tokio::test] +// async fn test_sql_candidate() { +// let state = AppState::new().await; +// let candidate = FormCandidate { +// lastname: "Fery".to_string(), +// firstname: "Yann".to_string(), +// }; +// let inserted_candidate = db_add_candidate(&state, candidate.clone()).await.unwrap(); +// assert_eq!(candidate, inserted_candidate); - let mut tera: Vec<&str> = [].to_vec(); - for tpl in state.tera.get_template_names() { - tera.push(tpl); - } - let mut list: Vec = [].to_vec(); - for tpl in Template::iter() { - list.push(tpl.display().to_string()); - } - assert_eq!(tera, list); -} +// } + +// #[tokio::test] +// async fn test_template_list() { +// let state = AppState::new().await; + +// let mut tera: Vec<&str> = [].to_vec(); +// for tpl in state.tera.get_template_names() { +// tera.push(tpl); +// } +// let mut list: Vec = [].to_vec(); +// for tpl in Template::iter() { +// list.push(tpl.display().to_string()); +// } +// assert_eq!(tera, list); +// } diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..af28bd2 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,17 @@ + + + + + + ENIM : Concours bac+2 + + + + + {% block content %} + {% endblock content %} + + diff --git a/templates/dossier.html b/templates/dossier.html index 3a0595e..d47b491 100644 --- a/templates/dossier.html +++ b/templates/dossier.html @@ -13,6 +13,6 @@
  • Prénom: {{ firstname }}
  • - Télécharger le dossier + Télécharger le dossier diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 6c414d4..0000000 --- a/templates/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - -
    - - - - - -
    - - diff --git a/templates/inscription.html b/templates/inscription.html new file mode 100644 index 0000000..6a76ea9 --- /dev/null +++ b/templates/inscription.html @@ -0,0 +1,191 @@ +{% extends "base.html" %} + +{% block content %} +
    + +
    + État civil + + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    +

    + Boursier + + + + +
    +

    + +

    + + +

    + +
    + +
    + Coordonnées + +

    Ces coordonnées seront utilisées pour toute correspondance concernant le concours

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    +
    + +
    + Scolarité + {% for year in ["2023/2024", "2022/2023", "2021/2022", "2020/2021"] %} +
    + Année scolaire {{ year }} + +

    + + +

    + +

    + + +

    + +

    + + +

    + +
    + {% endfor %} +
    + +
    + Niveau de langue + + {% for language in ["anglais", "allemand", "espagnol", "fle"] %} +
    + {{ language | capitalize}} + + + + + + + + +
    + {% endfor %} + +
    + +
    + Vœux des écoles + + {% for wish_number in [1, 2, 3] %} +

    + + + + + + + +

    + {% endfor %} + + +
    + +
    + Présentation autre concours +
    + + +
    +{%endblock content %} + +{% block css %} +.invalid { +background-color: ivory; +border: none; +outline: 2px solid red; +border-radius: 5px; +} +#inscription { +width: 1000px; +} +#inscription p { +display: flex; +justify-content: flex-end; +} +#inscription p label { +flex: 1; +} +#inscription p input { +flex: 1; +} +{% endblock css %} + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..b230e11 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block content %} +
    + + + + + +
    +{%endblock content %} + + +{% block css %} + .invalid { + background-color: ivory; + border: none; + outline: 2px solid red; + border-radius: 5px; + } +{% endblock css %} +