aboutsummaryrefslogtreecommitdiff
path: root/src/controller
diff options
context:
space:
mode:
authorJoris2021-01-03 13:40:40 +0100
committerJoris2021-01-03 13:54:20 +0100
commit11052951b74b9ad4b6a9412ae490086235f9154b (patch)
tree64526ac926c1bf470ea113f6cac8a33158684e8d /src/controller
parent371449b0e312a03162b78797b83dee9d81706669 (diff)
Rewrite in Rust
Diffstat (limited to 'src/controller')
-rw-r--r--src/controller/balance.rs71
-rw-r--r--src/controller/categories.rs141
-rw-r--r--src/controller/error.rs31
-rw-r--r--src/controller/incomes.rs221
-rw-r--r--src/controller/login.rs86
-rw-r--r--src/controller/mod.rs9
-rw-r--r--src/controller/payments.rs227
-rw-r--r--src/controller/statistics.rs30
-rw-r--r--src/controller/utils.rs119
-rw-r--r--src/controller/wallet.rs13
10 files changed, 948 insertions, 0 deletions
diff --git a/src/controller/balance.rs b/src/controller/balance.rs
new file mode 100644
index 0000000..228ff04
--- /dev/null
+++ b/src/controller/balance.rs
@@ -0,0 +1,71 @@
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::model::user::User;
+use crate::payer;
+use crate::templates;
+
+pub async fn get(wallet: &Wallet) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+
+ let incomes_from = db::incomes::defined_for_all(&wallet.pool).await;
+ let user_incomes = match incomes_from {
+ Some(from) => db::incomes::cumulative(&wallet.pool, from).await,
+ None => HashMap::new(),
+ };
+ let template_user_incomes =
+ get_template_user_incomes(&users, &user_incomes);
+ let total_income: i64 = user_incomes.values().sum();
+
+ let user_payments = db::payments::repartition(&wallet.pool).await;
+ let template_user_payments =
+ get_template_user_payments(&users, &user_payments);
+ let total_payments: i64 =
+ user_payments.clone().into_iter().map(|p| p.1).sum();
+
+ let exceeding_payers =
+ payer::exceeding(&users, &user_incomes, &user_payments);
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Balance);
+ context.insert("connected_user", &wallet.user);
+ context.insert(
+ "incomes_from",
+ &incomes_from.map(|d| d.format("%d/%m/%Y").to_string()),
+ );
+ context.insert("total_income", &total_income);
+ context.insert("user_incomes", &template_user_incomes);
+ context.insert("total_payments", &total_payments);
+ context.insert("user_payments", &template_user_payments);
+ context.insert("exceeding_payers", &exceeding_payers);
+
+ utils::template(&wallet.assets, &wallet.templates, "balance.html", context)
+}
+
+fn get_template_user_payments(
+ users: &Vec<User>,
+ user_payments: &HashMap<i64, i64>,
+) -> Vec<(String, i64)> {
+ let mut user_payments: Vec<(String, i64)> = users
+ .into_iter()
+ .map(|u| (u.name.clone(), *user_payments.get(&u.id).unwrap_or(&0)))
+ .collect();
+ user_payments.sort_by_key(|i| i.1);
+ user_payments
+}
+
+fn get_template_user_incomes(
+ users: &Vec<User>,
+ user_incomes: &HashMap<i64, i64>,
+) -> Vec<(String, i64)> {
+ let mut user_incomes: Vec<(String, i64)> = users
+ .into_iter()
+ .map(|u| (u.name.clone(), *user_incomes.get(&u.id).unwrap_or(&0)))
+ .collect();
+ user_incomes.sort_by_key(|i| i.1);
+ user_incomes
+}
diff --git a/src/controller/categories.rs b/src/controller/categories.rs
new file mode 100644
index 0000000..b1a3664
--- /dev/null
+++ b/src/controller/categories.rs
@@ -0,0 +1,141 @@
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::queries;
+use crate::templates;
+use crate::validation;
+
+pub async fn table(
+ wallet: &Wallet,
+ query: queries::Categories,
+) -> Response<Body> {
+ let categories = db::categories::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Categories);
+ context.insert("connected_user", &wallet.user);
+ context.insert("categories", &categories);
+ context.insert("highlight", &query.highlight);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "category/table.html",
+ context,
+ )
+}
+
+pub async fn create_form(wallet: &Wallet) -> Response<Body> {
+ create_form_feedback(wallet, HashMap::new(), None).await
+}
+
+async fn create_form_feedback(
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Categories);
+ context.insert("connected_user", &wallet.user.clone());
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "category/create.html",
+ context,
+ )
+}
+
+pub async fn create(
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ create_form_feedback(wallet, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::category::create(&form) {
+ Some(category) => {
+ match db::categories::create(&wallet.pool, &category).await {
+ Some(id) => {
+ utils::redirect(&format!("/categories?highlight={}", id))
+ }
+ None => error("Erreur serveur").await,
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn update_form(id: i64, wallet: &Wallet) -> Response<Body> {
+ update_form_feedback(id, wallet, HashMap::new(), None).await
+}
+
+async fn update_form_feedback(
+ id: i64,
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let category = db::categories::get(&wallet.pool, id).await;
+ let is_category_used =
+ db::payments::is_category_used(&wallet.pool, id).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Categories);
+ context.insert("connected_user", &wallet.user);
+ context.insert("id", &id);
+ context.insert("category", &category);
+ context.insert("is_category_used", &is_category_used);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "category/update.html",
+ context,
+ )
+}
+
+pub async fn update(
+ id: i64,
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ update_form_feedback(id, wallet, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::category::update(&form) {
+ Some(update_category) => {
+ if db::categories::update(&wallet.pool, id, &update_category).await
+ {
+ utils::redirect(&format!("/categories?highlight={}", id))
+ } else {
+ error("Erreur serveur").await
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn delete(id: i64, wallet: &Wallet) -> Response<Body> {
+ if db::categories::delete(&wallet.pool, id).await {
+ utils::redirect("/categories")
+ } else {
+ update_form_feedback(
+ id,
+ wallet,
+ HashMap::new(),
+ Some("Erreur serveur".to_string()),
+ )
+ .await
+ }
+}
diff --git a/src/controller/error.rs b/src/controller/error.rs
new file mode 100644
index 0000000..8dad16b
--- /dev/null
+++ b/src/controller/error.rs
@@ -0,0 +1,31 @@
+use hyper::header::CACHE_CONTROL;
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::{Context, Tera};
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+
+pub fn error(wallet: &Wallet, title: &str, message: &str) -> Response<Body> {
+ utils::with_header(
+ Response::new(
+ template(&wallet.assets, &wallet.templates, title, message).into(),
+ ),
+ CACHE_CONTROL,
+ "no-cache",
+ )
+}
+
+pub fn template(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ title: &str,
+ message: &str,
+) -> String {
+ let mut context = Context::new();
+ context.insert("title", title);
+ context.insert("message", message);
+ context.insert("assets", assets);
+
+ templates.render("error.html", &context).unwrap()
+}
diff --git a/src/controller/incomes.rs b/src/controller/incomes.rs
new file mode 100644
index 0000000..ea7f1cf
--- /dev/null
+++ b/src/controller/incomes.rs
@@ -0,0 +1,221 @@
+use chrono::Datelike;
+use chrono::Utc;
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::queries;
+use crate::templates;
+use crate::validation;
+
+static PER_PAGE: i64 = 10;
+
+pub async fn table(wallet: &Wallet, query: queries::Incomes) -> Response<Body> {
+ let page = query.page.unwrap_or(1);
+ let count = db::incomes::count(&wallet.pool).await;
+ let incomes = db::incomes::list(&wallet.pool, page, PER_PAGE).await;
+ let max_page = (count as f32 / PER_PAGE as f32).ceil() as i64;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Incomes);
+ context.insert("connected_user", &wallet.user);
+ context.insert("incomes", &incomes);
+ context.insert("page", &page);
+ context.insert("max_page", &max_page);
+ context.insert("highlight", &query.highlight);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "income/table.html",
+ context,
+ )
+}
+
+static MONTHS: [&str; 12] = [
+ "Janvier",
+ "Février",
+ "Mars",
+ "Avril",
+ "Mai",
+ "Juin",
+ "Juillet",
+ "Août",
+ "Septembre",
+ "Octobre",
+ "Novembre",
+ "Décembre",
+];
+
+pub async fn create_form(
+ wallet: &Wallet,
+ query: queries::Incomes,
+) -> Response<Body> {
+ create_form_feedback(wallet, query, HashMap::new(), None).await
+}
+
+async fn create_form_feedback(
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Incomes);
+ context.insert("connected_user", &wallet.user);
+ context.insert("users", &users);
+ context.insert("query", &query);
+ context.insert("current_month", &Utc::today().naive_utc().month());
+ context.insert("months", &MONTHS);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "income/create.html",
+ context,
+ )
+}
+
+pub async fn create(
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ create_form_feedback(wallet, query, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::income::create(&form) {
+ Some(income) => {
+ if !db::incomes::defined_at(
+ &wallet.pool,
+ income.user_id,
+ income.month,
+ income.year,
+ )
+ .await
+ .is_empty()
+ {
+ error("Un revenu est déjà défini à cette date.").await
+ } else {
+ match db::incomes::create(&wallet.pool, &income).await {
+ Some(id) => {
+ let row = db::incomes::get_row(&wallet.pool, id).await;
+ let page = (row - 1) / PER_PAGE + 1;
+ utils::redirect(&format!(
+ "/incomes?page={}&highlight={}",
+ page, id
+ ))
+ }
+ None => error("Erreur serveur").await,
+ }
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn update_form(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+) -> Response<Body> {
+ update_form_feedback(id, wallet, query, HashMap::new(), None).await
+}
+
+async fn update_form_feedback(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+ let income = db::incomes::get(&wallet.pool, id).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Incomes);
+ context.insert("connected_user", &wallet.user);
+ context.insert("users", &users);
+ context.insert("id", &id);
+ context.insert("income", &income);
+ context.insert("query", &query);
+ context.insert("months", &MONTHS);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "income/update.html",
+ context,
+ )
+}
+
+pub async fn update(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ update_form_feedback(
+ id,
+ wallet,
+ query,
+ form.clone(),
+ Some(e.to_string()),
+ )
+ };
+
+ match validation::income::update(&form) {
+ Some(income) => {
+ let existing_incomes = db::incomes::defined_at(
+ &wallet.pool,
+ income.user_id,
+ income.month,
+ income.year,
+ )
+ .await;
+ if existing_incomes.into_iter().any(|eid| eid != id) {
+ error("Un revenu est déjà défini à cette date.").await
+ } else if db::incomes::update(&wallet.pool, id, &income).await {
+ let row = db::incomes::get_row(&wallet.pool, id).await;
+ let page = (row - 1) / PER_PAGE + 1;
+ utils::redirect(&format!(
+ "/incomes?page={}&highlight={}",
+ page, id
+ ))
+ } else {
+ error("Erreur serveur").await
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn delete(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+) -> Response<Body> {
+ if db::incomes::delete(&wallet.pool, id).await {
+ utils::redirect(&format!("/incomes?page={}", query.page.unwrap_or(1)))
+ } else {
+ update_form_feedback(
+ id,
+ wallet,
+ query,
+ HashMap::new(),
+ Some("Erreur serveur".to_string()),
+ )
+ .await
+ }
+}
diff --git a/src/controller/login.rs b/src/controller/login.rs
new file mode 100644
index 0000000..ea9db57
--- /dev/null
+++ b/src/controller/login.rs
@@ -0,0 +1,86 @@
+use bcrypt;
+use hyper::{Body, Response};
+use sqlx::sqlite::SqlitePool;
+use std::collections::HashMap;
+use tera::{Context, Tera};
+use uuid::Uuid;
+
+use crate::controller::wallet::Wallet;
+use crate::controller::{error, utils};
+use crate::db;
+use crate::model::config::Config;
+use crate::model::user::User;
+use crate::validation;
+
+pub async fn page(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ error: Option<String>,
+) -> Response<Body> {
+ let connected_user: Option<User> = None;
+
+ let mut context = Context::new();
+ context.insert("connected_user", &connected_user);
+ context.insert("error", &error);
+
+ utils::template(assets, templates, "login.html", context)
+}
+
+pub async fn login(
+ config: Config,
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ form: HashMap<String, String>,
+ pool: SqlitePool,
+) -> Response<Body> {
+ let not_authorized = page(
+ assets,
+ templates,
+ Some("Vous n’êtes pas autorisé à vous connecter.".to_string()),
+ )
+ .await;
+ let server_error =
+ page(assets, templates, Some("Erreur serveur.".to_string())).await;
+ match validation::login::login(&form) {
+ Some(login) => {
+ match db::users::get_password_hash(&pool, login.email.clone()).await
+ {
+ Some(hash) => match bcrypt::verify(login.password, &hash) {
+ Ok(true) => {
+ let login_token = Uuid::new_v4();
+ if db::users::set_login_token(
+ &pool,
+ login.email,
+ login_token.clone().to_string(),
+ )
+ .await
+ {
+ utils::with_login_cookie(
+ config,
+ login_token,
+ utils::redirect("/"),
+ )
+ } else {
+ server_error
+ }
+ }
+ Ok(false) => not_authorized,
+ Err(err) => {
+ error!("Error verifying bcrypt password: {:?}", err);
+ server_error
+ }
+ },
+ None => not_authorized,
+ }
+ }
+ None => not_authorized,
+ }
+}
+
+pub async fn logout(config: Config, wallet: &Wallet) -> Response<Body> {
+ if db::users::remove_login_token(&wallet.pool, wallet.user.id).await {
+ utils::with_logout_cookie(config, utils::redirect("/"))
+ } else {
+ error::error(&wallet, "Erreur serveur", "Erreur serveur")
+ }
+}
diff --git a/src/controller/mod.rs b/src/controller/mod.rs
new file mode 100644
index 0000000..e2ef561
--- /dev/null
+++ b/src/controller/mod.rs
@@ -0,0 +1,9 @@
+pub mod balance;
+pub mod categories;
+pub mod error;
+pub mod incomes;
+pub mod login;
+pub mod payments;
+pub mod statistics;
+pub mod utils;
+pub mod wallet;
diff --git a/src/controller/payments.rs b/src/controller/payments.rs
new file mode 100644
index 0000000..ab4bd92
--- /dev/null
+++ b/src/controller/payments.rs
@@ -0,0 +1,227 @@
+use hyper::header::CONTENT_TYPE;
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::model::frequency::Frequency;
+use crate::queries;
+use crate::templates;
+use crate::validation;
+
+static PER_PAGE: i64 = 10;
+
+pub async fn table(
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ let page = query.page.unwrap_or(1);
+ let count = db::payments::count(&wallet.pool, &query).await;
+ let payments =
+ db::payments::list_for_table(&wallet.pool, &query, PER_PAGE).await;
+ let max_page = (count.count as f32 / PER_PAGE as f32).ceil() as i64;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Payments);
+ context.insert("connected_user", &wallet.user);
+ context.insert("payments", &payments);
+ context.insert("page", &page);
+ context.insert("max_page", &max_page);
+ context.insert("query", &query);
+ context.insert("count", &count.count);
+ context.insert("total_cost", &count.total_cost);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "payment/table.html",
+ context,
+ )
+}
+
+pub async fn create_form(
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ create_form_feedback(wallet, query, HashMap::new(), None).await
+}
+
+async fn create_form_feedback(
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+ let categories = db::categories::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Payments);
+ context.insert("connected_user", &wallet.user);
+ context.insert("users", &users);
+ context.insert("categories", &categories);
+ context.insert("query", &query);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "payment/create.html",
+ context,
+ )
+}
+
+pub async fn create(
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ create_form_feedback(wallet, query, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::payment::create(&form) {
+ Some(create_payment) => {
+ match db::payments::create(&wallet.pool, &create_payment).await {
+ Some(id) => {
+ let row = db::payments::get_row(
+ &wallet.pool,
+ id,
+ create_payment.frequency,
+ )
+ .await;
+ let page = (row - 1) / PER_PAGE + 1;
+ let query = queries::Payments {
+ page: Some(page),
+ search: None,
+ frequency: Some(create_payment.frequency),
+ highlight: Some(id),
+ };
+ utils::redirect(&format!(
+ "/{}",
+ queries::payments_url(query)
+ ))
+ }
+ None => error("Erreur serveur.").await,
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn update_form(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ update_form_feedback(id, wallet, query, HashMap::new(), None).await
+}
+
+async fn update_form_feedback(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let payment = db::payments::get_for_form(&wallet.pool, id).await;
+ let users = db::users::list(&wallet.pool).await;
+ let categories = db::categories::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Payments);
+ context.insert("connected_user", &wallet.user);
+ context.insert("id", &id);
+ context.insert("payment", &payment);
+ context.insert("users", &users);
+ context.insert("categories", &categories);
+ context.insert("query", &query);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "payment/update.html",
+ context,
+ )
+}
+
+pub async fn update(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ update_form_feedback(
+ id,
+ wallet,
+ query.clone(),
+ form.clone(),
+ Some(e.to_string()),
+ )
+ };
+
+ match validation::payment::update(&form) {
+ Some(update_payment) => {
+ if db::payments::update(&wallet.pool, id, &update_payment).await {
+ let frequency = query.frequency.unwrap_or(Frequency::Punctual);
+ let row =
+ db::payments::get_row(&wallet.pool, id, frequency).await;
+ let page = (row - 1) / PER_PAGE + 1;
+ let query = queries::Payments {
+ page: Some(page),
+ search: None,
+ frequency: Some(frequency),
+ highlight: Some(id),
+ };
+ utils::redirect(&format!("/{}", queries::payments_url(query)))
+ } else {
+ error("Erreur serveur").await
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn delete(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ if db::payments::delete(&wallet.pool, id).await {
+ let query = queries::Payments {
+ highlight: None,
+ ..query
+ };
+ utils::redirect(&format!("/{}", queries::payments_url(query)))
+ } else {
+ update_form_feedback(
+ id,
+ wallet,
+ query,
+ HashMap::new(),
+ Some("Erreur serveur".to_string()),
+ )
+ .await
+ }
+}
+
+pub async fn search_category(
+ wallet: &Wallet,
+ query: queries::PaymentCategory,
+) -> Response<Body> {
+ match db::payments::search_category(&wallet.pool, query.payment_name).await
+ {
+ Some(category_id) => utils::with_header(
+ Response::new(format!("{}", category_id).into()),
+ CONTENT_TYPE,
+ "application/json",
+ ),
+ None => utils::not_found(),
+ }
+}
diff --git a/src/controller/statistics.rs b/src/controller/statistics.rs
new file mode 100644
index 0000000..38a5787
--- /dev/null
+++ b/src/controller/statistics.rs
@@ -0,0 +1,30 @@
+use hyper::{Body, Response};
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::templates;
+
+pub async fn get(wallet: &Wallet) -> Response<Body> {
+ let categories = db::categories::list(&wallet.pool).await;
+ let payments = db::payments::list_for_stats(&wallet.pool).await;
+ let incomes = db::incomes::total_each_month(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Statistics);
+ context.insert("connected_user", &wallet.user);
+ context.insert(
+ "json_categories",
+ &serde_json::to_string(&categories).unwrap(),
+ );
+ context.insert("json_payments", &serde_json::to_string(&payments).unwrap());
+ context.insert("json_incomes", &serde_json::to_string(&incomes).unwrap());
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "statistics.html",
+ context,
+ )
+}
diff --git a/src/controller/utils.rs b/src/controller/utils.rs
new file mode 100644
index 0000000..225f8a4
--- /dev/null
+++ b/src/controller/utils.rs
@@ -0,0 +1,119 @@
+use hyper::header::{
+ HeaderName, HeaderValue, CACHE_CONTROL, CONTENT_TYPE, LOCATION, SET_COOKIE,
+};
+use hyper::{Body, Response, StatusCode};
+use std::collections::HashMap;
+use tera::{Context, Tera};
+use tokio::fs::File;
+use tokio_util::codec::{BytesCodec, FramedRead};
+use uuid::Uuid;
+
+use crate::controller::error;
+use crate::model::config::Config;
+
+pub fn with_header(
+ response: Response<Body>,
+ name: HeaderName,
+ value: &str,
+) -> Response<Body> {
+ with_headers(response, vec![(name, value)])
+}
+
+pub fn with_headers(
+ response: Response<Body>,
+ headers: Vec<(HeaderName, &str)>,
+) -> Response<Body> {
+ let mut response = response;
+ let response_headers = response.headers_mut();
+ for (name, value) in headers {
+ response_headers.insert(name, HeaderValue::from_str(value).unwrap());
+ }
+ response
+}
+
+pub fn with_login_cookie(
+ config: Config,
+ login_token: Uuid,
+ response: Response<Body>,
+) -> Response<Body> {
+ let cookie = format!(
+ "TOKEN={}; SameSite=Strict; HttpOnly; Max-Age=86400{}",
+ login_token,
+ if config.secure_cookies {
+ "; Secure"
+ } else {
+ ""
+ }
+ );
+
+ with_header(response, SET_COOKIE, &cookie)
+}
+
+pub fn with_logout_cookie(
+ config: Config,
+ response: Response<Body>,
+) -> Response<Body> {
+ let cookie = format!(
+ "TOKEN=; SameSite=Strict; HttpOnly; Max-Age=0{}",
+ if config.secure_cookies {
+ "; Secure"
+ } else {
+ ""
+ }
+ );
+
+ with_header(response, SET_COOKIE, &cookie)
+}
+
+pub fn template(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ path: &str,
+ context: Context,
+) -> Response<Body> {
+ let mut context = context.clone();
+ context.insert("assets", assets);
+
+ let response = match templates.render(path, &context) {
+ Ok(template) => Response::new(template.into()),
+ Err(err) => Response::new(
+ error::template(
+ assets,
+ templates,
+ "Erreur serveur",
+ &format!(
+ "Erreur lors de la préparation de la page : {:?}",
+ err
+ ),
+ )
+ .into(),
+ ),
+ };
+
+ with_headers(
+ response,
+ vec![(CONTENT_TYPE, "text/html"), (CACHE_CONTROL, "no-cache")],
+ )
+}
+
+pub fn redirect(uri: &str) -> Response<Body> {
+ let mut response = Response::default();
+ *response.status_mut() = StatusCode::MOVED_PERMANENTLY;
+ with_headers(response, vec![(LOCATION, uri), (CACHE_CONTROL, "no-cache")])
+}
+
+pub fn not_found() -> Response<Body> {
+ let mut response = Response::default();
+ *response.status_mut() = StatusCode::NOT_FOUND;
+ response
+}
+
+pub async fn file(filename: &str) -> Response<Body> {
+ if let Ok(file) = File::open(filename).await {
+ let stream = FramedRead::new(file, BytesCodec::new());
+ let body = Body::wrap_stream(stream);
+ with_header(Response::new(body), CACHE_CONTROL, "max-age=3153600000")
+ } else {
+ not_found()
+ }
+}
diff --git a/src/controller/wallet.rs b/src/controller/wallet.rs
new file mode 100644
index 0000000..2a4a593
--- /dev/null
+++ b/src/controller/wallet.rs
@@ -0,0 +1,13 @@
+use sqlx::sqlite::SqlitePool;
+use std::collections::HashMap;
+use tera::Tera;
+
+use crate::model::user::User;
+
+#[derive(Clone)]
+pub struct Wallet {
+ pub pool: SqlitePool,
+ pub assets: HashMap<String, String>,
+ pub templates: Tera,
+ pub user: User,
+}