aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/controller/login.rs3
-rw-r--r--src/controller/utils.rs4
-rw-r--r--src/db/incomes.rs67
-rw-r--r--src/db/jobs.rs1
-rw-r--r--src/db/migrations/01-init.sql65
-rw-r--r--src/db/migrations/02-payment-category.sql44
-rw-r--r--src/db/migrations/03-sign-in-token.sql5
-rw-r--r--src/db/migrations/04-plural-naming.sql91
-rw-r--r--src/db/migrations/05-strict-tables.sql107
-rw-r--r--src/db/migrations/06-remove-weekly-report-job.sql1
-rw-r--r--src/db/mod.rs49
-rw-r--r--src/db/payments.rs73
-rw-r--r--src/db/utils.rs16
-rw-r--r--src/jobs/mod.rs16
-rw-r--r--src/jobs/weekly_report.rs65
-rw-r--r--src/mail.rs104
-rw-r--r--src/main.rs17
-rw-r--r--src/model/config.rs2
-rw-r--r--src/model/job.rs1
-rw-r--r--src/model/mod.rs1
-rw-r--r--src/model/report.rs40
-rw-r--r--src/routes.rs2
-rw-r--r--src/templates.rs28
-rw-r--r--src/utils/cookie.rs12
24 files changed, 408 insertions, 406 deletions
diff --git a/src/controller/login.rs b/src/controller/login.rs
index d01f799..f7e0695 100644
--- a/src/controller/login.rs
+++ b/src/controller/login.rs
@@ -44,7 +44,8 @@ pub async fn login(
{
Some(hash) => match bcrypt::verify(login.password, &hash) {
Ok(true) => {
- let login_token = cookie::generate_token();
+ // TODO: error handling
+ let login_token = cookie::generate_token().unwrap();
if db::users::set_login_token(
&db_conn,
diff --git a/src/controller/utils.rs b/src/controller/utils.rs
index 340a5c7..ccef33c 100644
--- a/src/controller/utils.rs
+++ b/src/controller/utils.rs
@@ -71,8 +71,8 @@ fn server_error(
)
}
-pub fn text(str: String) -> Response<Full<Bytes>> {
- let mut response = Response::new(str.into());
+pub fn text(str: impl Into<String>) -> Response<Full<Bytes>> {
+ let mut response = Response::new(str.into().into());
*response.status_mut() = StatusCode::OK;
response
}
diff --git a/src/db/incomes.rs b/src/db/incomes.rs
index 90282c0..d33cbcb 100644
--- a/src/db/incomes.rs
+++ b/src/db/incomes.rs
@@ -5,7 +5,6 @@ use tokio_rusqlite::{named_params, Connection, Row};
use crate::db::utils;
use crate::model::income::{Create, Form, Stat, Table, Update};
-use crate::model::report::Report;
fn row_to_table(row: &Row) -> Result<Table, rusqlite::Error> {
Ok(Table {
@@ -373,8 +372,8 @@ fn cumulative_query(from: NaiveDate) -> String {
ON
users.id = incomes.user_id
"#,
- bounded_query(">".to_string(), from.format("%Y-%m-%d").to_string()),
- bounded_query("<".to_string(), "date()".to_string())
+ bounded_query(">", &from.format("%Y-%m-%d").to_string()),
+ bounded_query("<", "date()")
)
}
@@ -382,7 +381,7 @@ fn cumulative_query(from: NaiveDate) -> String {
///
/// It filters incomes according to the operator and date,
/// and adds the income at this date.
-fn bounded_query(op: String, date: String) -> String {
+fn bounded_query(op: &str, date: &str) -> String {
format!(
r#"
SELECT
@@ -487,63 +486,3 @@ pub async fn total_each_month(conn: &Connection) -> Vec<Stat> {
}
}
}
-
-pub async fn last_week(conn: &Connection) -> Vec<Report> {
- let query = r#"
- SELECT
- strftime('%m/%Y', incomes.date) AS date,
- users.name AS name,
- incomes.amount AS amount,
- (CASE
- WHEN
- incomes.deleted_at IS NOT NULL
- THEN
- 'Deleted'
- WHEN
- incomes.updated_at IS NOT NULL
- AND incomes.created_at < date('now', 'weekday 0', '-13 days')
- THEN
- 'Updated'
- ELSE
- 'Created'
- END) AS action
- FROM
- incomes
- INNER JOIN
- users
- ON
- incomes.user_id = users.id
- WHERE
- (
- incomes.created_at >= date('now', 'weekday 0', '-13 days')
- AND incomes.created_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- incomes.updated_at >= date('now', 'weekday 0', '-13 days')
- AND incomes.updated_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- incomes.deleted_at >= date('now', 'weekday 0', '-13 days')
- AND incomes.deleted_at < date('now', 'weekday 0', '-6 days')
- )
- ORDER BY
- incomes.date
- "#;
-
- let res = conn
- .call(move |conn| {
- let mut stmt = conn.prepare(query)?;
- let xs = stmt
- .query_map([], utils::row_to_report)?
- .collect::<Result<Vec<Report>, _>>()?;
-
- Ok(xs)
- })
- .await;
-
- match res {
- Ok(xs) => xs,
- Err(err) => {
- log::error!("Error listing payments for report: {:?}", err);
- vec![]
- }
- }
-}
diff --git a/src/db/jobs.rs b/src/db/jobs.rs
index 1d00408..0080339 100644
--- a/src/db/jobs.rs
+++ b/src/db/jobs.rs
@@ -4,7 +4,6 @@ use crate::model::job::Job;
pub async fn should_run(conn: &Connection, job: Job) -> bool {
let run_from = match job {
- Job::WeeklyReport => "date('now', 'weekday 0', '-6 days')",
Job::MonthlyPayment => "date('now', 'start of month')",
};
diff --git a/src/db/migrations/01-init.sql b/src/db/migrations/01-init.sql
new file mode 100644
index 0000000..d7c300e
--- /dev/null
+++ b/src/db/migrations/01-init.sql
@@ -0,0 +1,65 @@
+CREATE TABLE IF NOT EXISTS "user" (
+ "id" INTEGER PRIMARY KEY,
+ "creation" TIMESTAMP NOT NULL,
+ "email" VARCHAR NOT NULL,
+ "name" VARCHAR NOT NULL,
+ CONSTRAINT "uniq_user_email" UNIQUE ("email"),
+ CONSTRAINT "uniq_user_name" UNIQUE ("name")
+);
+
+CREATE TABLE IF NOT EXISTS "job" (
+ "id" INTEGER PRIMARY KEY,
+ "kind" VARCHAR NOT NULL,
+ "last_execution" TIMESTAMP NULL,
+ "last_check" TIMESTAMP NULL,
+ CONSTRAINT "uniq_job_kind" UNIQUE ("kind")
+);
+
+CREATE TABLE IF NOT EXISTS "sign_in"(
+ "id" INTEGER PRIMARY KEY,
+ "token" VARCHAR NOT NULL,
+ "creation" TIMESTAMP NOT NULL,
+ "email" VARCHAR NOT NULL,
+ "is_used" BOOLEAN NOT NULL,
+ CONSTRAINT "uniq_sign_in_token" UNIQUE ("token")
+);
+
+CREATE TABLE IF NOT EXISTS "payment"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "user",
+ "name" VARCHAR NOT NULL,
+ "cost" INTEGER NOT NULL,
+ "date" DATE NOT NULL,
+ "frequency" VARCHAR NOT NULL,
+ "created_at" TIMESTAMP NOT NULL,
+ "edited_at" TIMESTAMP NULL,
+ "deleted_at" TIMESTAMP NULL
+);
+
+CREATE TABLE IF NOT EXISTS "income"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "user",
+ "date" DATE NOT NULL,
+ "amount" INTEGERNOT NULL,
+ "created_at" TIMESTAMP NOT NULL,
+ "edited_at" TIMESTAMP NULL,
+ "deleted_at" TIMESTAMP NULL
+);
+
+CREATE TABLE IF NOT EXISTS "category"(
+ "id" INTEGER PRIMARY KEY,
+ "name" VARCHAR NOT NULL,
+ "color" VARCHAR NOT NULL,
+ "created_at" TIMESTAMP NOT NULL,
+ "edited_at" TIMESTAMP NULL,
+ "deleted_at" TIMESTAMP NULL
+);
+
+CREATE TABLE IF NOT EXISTS "payment_category"(
+ "id" INTEGER PRIMARY KEY,
+ "name" VARCHAR NOT NULL,
+ "category" INTEGER NOT NULL REFERENCES "category",
+ "created_at" TIMESTAMP NOT NULL,
+ "edited_at" TIMESTAMP NULL,
+ CONSTRAINT "uniq_payment_category_name" UNIQUE ("name")
+);
diff --git a/src/db/migrations/02-payment-category.sql b/src/db/migrations/02-payment-category.sql
new file mode 100644
index 0000000..c1d502f
--- /dev/null
+++ b/src/db/migrations/02-payment-category.sql
@@ -0,0 +1,44 @@
+-- Add payment categories with accents from payment with accents
+
+INSERT INTO
+ payment_category (name, category, created_at)
+SELECT
+ DISTINCT lower(payment.name), payment_category.category, datetime('now')
+FROM
+ payment
+INNER JOIN
+ payment_category
+ON
+ replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower(payment.name), 'é', 'e'), 'è', 'e'), 'à', 'a'), 'û', 'u'), 'â', 'a'), 'ê', 'e'), 'â', 'a'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ë', 'e') = payment_category.name
+WHERE
+ payment.name
+IN
+ (SELECT DISTINCT payment.name FROM payment WHERE lower(payment.name) NOT IN (SELECT payment_category.name FROM payment_category) AND payment.deleted_at IS NULL);
+
+-- Remove unused payment categories
+
+DELETE FROM
+ payment_category
+WHERE
+ name NOT IN (SELECT DISTINCT lower(name) FROM payment);
+
+-- Add category id to payment table
+
+PRAGMA foreign_keys = 0;
+
+ALTER TABLE payment ADD COLUMN "category" INTEGER NOT NULL REFERENCES "category" DEFAULT -1;
+
+PRAGMA foreign_keys = 1;
+
+UPDATE
+ payment
+SET
+ category = (SELECT category FROM payment_category WHERE payment_category.name = LOWER(payment.name))
+WHERE
+ EXISTS (SELECT category FROM payment_category WHERE payment_category.name = LOWER(payment.name));
+
+DELETE FROM payment WHERE category = -1;
+
+-- Remove
+
+DROP TABLE payment_category;
diff --git a/src/db/migrations/03-sign-in-token.sql b/src/db/migrations/03-sign-in-token.sql
new file mode 100644
index 0000000..a3d8a13
--- /dev/null
+++ b/src/db/migrations/03-sign-in-token.sql
@@ -0,0 +1,5 @@
+DROP TABLE sign_in;
+
+ALTER TABLE user ADD COLUMN "password" TEXT NOT NULL DEFAULT "password";
+
+ALTER TABLE user ADD COLUMN "sign_in_token" TEXT NULL;
diff --git a/src/db/migrations/04-plural-naming.sql b/src/db/migrations/04-plural-naming.sql
new file mode 100644
index 0000000..ec386cb
--- /dev/null
+++ b/src/db/migrations/04-plural-naming.sql
@@ -0,0 +1,91 @@
+-- Payments
+
+CREATE TABLE IF NOT EXISTS "payments"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "users",
+ "name" TEXT NOT NULL,
+ "cost" INTEGER NOT NULL,
+ "date" DATE NOT NULL,
+ "frequency" TEXT NOT NULL,
+ "category_id" INTEGER NOT NULL REFERENCES "categories",
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL
+);
+
+INSERT INTO payments (id, user_id, name, cost, date, frequency, category_id, created_at, updated_at, deleted_at)
+ SELECT id, user_id, name, cost, date, frequency, category, created_at, edited_at, deleted_at
+ FROM payment;
+
+DROP TABLE payment;
+
+CREATE INDEX payment_date ON payments(date);
+
+-- Categories
+
+CREATE TABLE IF NOT EXISTS "categories"(
+ "id" INTEGER PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "color" TEXT NOT NULL,
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL
+);
+
+INSERT INTO categories (id, name, color, created_at, updated_at, deleted_at)
+ SELECT id, name, color, created_at, edited_at, deleted_at
+ FROM category;
+
+DROP TABLE category;
+
+-- Users
+
+CREATE TABLE IF NOT EXISTS "users"(
+ "id" INTEGER PRIMARY KEY,
+ "email" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "login_token" TEXT NULL,
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL,
+ CONSTRAINT "uniq_user_email" UNIQUE ("email"),
+ CONSTRAINT "uniq_user_name" UNIQUE ("name")
+);
+
+INSERT INTO users (id, created_at, email, name, password, login_token)
+ SELECT id, creation, email, name, password, sign_in_token
+ FROM user;
+
+DROP TABLE user;
+
+-- Jobs
+
+CREATE TABLE IF NOT EXISTS "jobs"(
+ "name" TEXT PRIMARY KEY,
+ "last_execution" DATE NOT NULL DEFAULT (datetime('now'))
+);
+
+INSERT INTO jobs (name, last_execution)
+ SELECT kind, last_execution
+ FROM job;
+
+DROP TABLE job;
+
+-- Incomes
+
+CREATE TABLE IF NOT EXISTS "incomes"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "users",
+ "date" DATE NOT NULL,
+ "amount" INTEGER NOT NULL,
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL
+);
+
+INSERT INTO incomes (id, user_id, date, amount, created_at, updated_at, deleted_at)
+ SELECT id, user_id, date, amount, created_at, edited_at, deleted_at
+ FROM income;
+
+DROP TABLE income;
diff --git a/src/db/migrations/05-strict-tables.sql b/src/db/migrations/05-strict-tables.sql
new file mode 100644
index 0000000..cf7ef4b
--- /dev/null
+++ b/src/db/migrations/05-strict-tables.sql
@@ -0,0 +1,107 @@
+-- Activate strict mode
+
+-- Start with users and categories, as it’s referenced in other tables.
+-- Otherwise, the reference is set to the renamed non strict table.
+
+-- Users
+
+ALTER TABLE "users" RENAME TO "users_non_strict";
+
+CREATE TABLE IF NOT EXISTS "users"(
+ "id" INTEGER PRIMARY KEY,
+ "email" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "login_token" TEXT NULL,
+ "created_at" TEXT NULL DEFAULT (datetime('now')),
+ "updated_at" TEXT NULL,
+ "deleted_at" TEXT NULL,
+ CONSTRAINT "uniq_user_email" UNIQUE ("email"),
+ CONSTRAINT "uniq_user_name" UNIQUE ("name")
+) STRICT;
+
+INSERT INTO users (id, created_at, email, name, password, login_token)
+ SELECT id, created_at, email, name, password, login_token
+ FROM users_non_strict;
+
+DROP TABLE users_non_strict;
+
+-- Categories
+
+ALTER TABLE "categories" RENAME TO "categories_non_strict";
+
+CREATE TABLE IF NOT EXISTS "categories"(
+ "id" INTEGER PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "color" TEXT NOT NULL,
+ "created_at" TEXT NULL DEFAULT (datetime('now')),
+ "updated_at" TEXT NULL,
+ "deleted_at" TEXT NULL
+) STRICT;
+
+INSERT INTO categories (id, name, color, created_at, updated_at, deleted_at)
+ SELECT id, name, color, created_at, updated_at, deleted_at
+ FROM categories_non_strict;
+
+DROP TABLE categories_non_strict;
+
+-- Payments
+
+ALTER TABLE "payments" RENAME TO "payments_non_strict";
+
+CREATE TABLE IF NOT EXISTS "payments"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "users",
+ "name" TEXT NOT NULL,
+ "cost" INTEGER NOT NULL,
+ "date" TEXT NOT NULL,
+ "frequency" TEXT NOT NULL,
+ "category_id" INTEGER NOT NULL REFERENCES "categories",
+ "created_at" TEXT NULL DEFAULT (datetime('now')),
+ "updated_at" TEXT NULL,
+ "deleted_at" TEXT NULL
+) STRICT;
+
+DROP INDEX IF EXISTS payment_date;
+CREATE INDEX payment_date ON payments(date);
+
+INSERT INTO payments (id, user_id, name, cost, date, frequency, category_id, created_at, updated_at, deleted_at)
+ SELECT id, user_id, name, cost, date, frequency, category_id, created_at, updated_at, deleted_at
+ FROM payments_non_strict;
+
+DROP TABLE payments_non_strict;
+
+-- Jobs
+
+ALTER TABLE "jobs" RENAME TO "jobs_non_strict";
+
+CREATE TABLE IF NOT EXISTS "jobs"(
+ "name" TEXT PRIMARY KEY,
+ "last_execution" TEXT NOT NULL DEFAULT (datetime('now'))
+) STRICT;
+
+INSERT INTO jobs (name, last_execution)
+ SELECT name, last_execution
+ FROM jobs_non_strict;
+
+DROP TABLE jobs_non_strict;
+
+-- Incomes
+
+ALTER TABLE "incomes" RENAME TO "incomes_non_strict";
+
+CREATE TABLE IF NOT EXISTS "incomes"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "users",
+ "date" TEXT NOT NULL,
+ "amount" INTEGER NOT NULL,
+ "created_at" TEXT NULL DEFAULT (datetime('now')),
+ "updated_at" TEXT NULL,
+ "deleted_at" TEXT NULL
+) STRICT;
+
+INSERT INTO incomes (id, user_id, date, amount, created_at, updated_at, deleted_at)
+ SELECT id, user_id, date, amount, created_at, updated_at, deleted_at
+ FROM incomes_non_strict;
+
+DROP TABLE incomes_non_strict;
diff --git a/src/db/migrations/06-remove-weekly-report-job.sql b/src/db/migrations/06-remove-weekly-report-job.sql
new file mode 100644
index 0000000..415ea76
--- /dev/null
+++ b/src/db/migrations/06-remove-weekly-report-job.sql
@@ -0,0 +1 @@
+DELETE FROM jobs WHERE name = 'WeeklyReport';
diff --git a/src/db/mod.rs b/src/db/mod.rs
index a0aa3dc..4894e95 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -1,6 +1,55 @@
+use anyhow::{Error, Result};
+use rusqlite_migration::{Migrations, M};
+use tokio_rusqlite::Connection;
+
pub mod categories;
pub mod incomes;
pub mod jobs;
pub mod payments;
pub mod users;
mod utils;
+
+pub async fn init(path: &str) -> Result<Connection> {
+ let connection = Connection::open(path).await.map_err(|err| {
+ Error::msg(format!("Error opening connection: {err}"))
+ })?;
+
+ apply_migrations(&connection).await?;
+ set_pragma(&connection, "foreign_keys", "ON").await?;
+ set_pragma(&connection, "journal_mode", "wal").await?;
+ Ok(connection)
+}
+
+async fn apply_migrations(conn: &Connection) -> Result<()> {
+ let migrations = Migrations::new(vec![
+ M::up(include_str!("migrations/01-init.sql")),
+ M::up(include_str!("migrations/02-payment-category.sql")),
+ M::up(include_str!("migrations/03-sign-in-token.sql")),
+ M::up(include_str!("migrations/04-plural-naming.sql")),
+ M::up(include_str!("migrations/05-strict-tables.sql")),
+ M::up(include_str!("migrations/06-remove-weekly-report-job.sql")),
+ ]);
+
+ Ok(conn
+ .call(move |conn| {
+ migrations.to_latest(conn).map_err(|migration_err| {
+ tokio_rusqlite::Error::Other(Box::new(migration_err))
+ })
+ })
+ .await?)
+}
+
+async fn set_pragma(
+ conn: &Connection,
+ key: impl Into<String>,
+ value: impl Into<String>,
+) -> Result<()> {
+ let key = key.into();
+ let value = value.into();
+ Ok(conn
+ .call(move |conn| {
+ conn.pragma_update(None, &key, &value)
+ .map_err(tokio_rusqlite::Error::Rusqlite)
+ })
+ .await?)
+}
diff --git a/src/db/payments.rs b/src/db/payments.rs
index 4a6774c..23b4d2f 100644
--- a/src/db/payments.rs
+++ b/src/db/payments.rs
@@ -7,7 +7,6 @@ use tokio_rusqlite::{
use crate::db::utils;
use crate::model::frequency::Frequency;
use crate::model::payment;
-use crate::model::report::Report;
use crate::queries;
use crate::utils::text;
@@ -61,9 +60,7 @@ pub async fn count(
payment_query: &queries::Payments,
) -> Count {
let mut query = r#"
- SELECT
- COUNT(*) AS count,
- SUM(payments.cost) AS total_cost
+ SELECT COUNT(*), SUM(payments.cost)
FROM payments
INNER JOIN users ON users.id = payments.user_id
INNER JOIN categories ON categories.id = payments.category_id
@@ -85,13 +82,10 @@ pub async fn count(
match res {
Ok(count) => count,
- Err(err) => {
- log::error!("Error counting payments: {:?}", err);
- Count {
- count: 0,
- total_cost: 0,
- }
- }
+ Err(_) => Count {
+ count: 0,
+ total_cost: 0,
+ },
}
}
@@ -192,7 +186,7 @@ fn complete_name(
)
.as_str(),
);
- params.push(Box::new(name));
+ params.push(Box::new(text::format_search(&name)));
}
}
}
@@ -572,58 +566,3 @@ pub async fn create_monthly_payments(conn: &Connection) {
Err(err) => log::error!("Error creating monthly payments: {:?}", err),
}
}
-
-pub async fn last_week(conn: &Connection) -> Vec<Report> {
- let query = r#"
- SELECT
- strftime('%d/%m/%Y', payments.date) AS date,
- (payments.name || ' (' || users.name || ')') AS name,
- payments.cost AS amount,
- (CASE
- WHEN payments.deleted_at IS NOT NULL
- THEN 'Deleted'
- WHEN
- payments.updated_at IS NOT NULL
- AND payments.created_at < date('now', 'weekday 0', '-13 days')
- THEN 'Updated'
- ELSE 'Created'
- END) AS action
- FROM payments
- INNER JOIN users
- ON payments.user_id = users.id
- WHERE
- payments.frequency = 'Punctual'
- AND (
- (
- payments.created_at >= date('now', 'weekday 0', '-13 days')
- AND payments.created_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- payments.updated_at >= date('now', 'weekday 0', '-13 days')
- AND payments.updated_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- payments.deleted_at >= date('now', 'weekday 0', '-13 days')
- AND payments.deleted_at < date('now', 'weekday 0', '-6 days')
- )
- )
- ORDER BY payments.date
- "#;
-
- let res = conn
- .call(move |conn| {
- let mut stmt = conn.prepare(query)?;
- let xs = stmt
- .query_map([], utils::row_to_report)?
- .collect::<Result<Vec<Report>, _>>()?;
-
- Ok(xs)
- })
- .await;
-
- match res {
- Ok(payments) => payments,
- Err(err) => {
- log::error!("Error listing payments for report: {:?}", err);
- vec![]
- }
- }
-}
diff --git a/src/db/utils.rs b/src/db/utils.rs
index f61d20a..8f8a31d 100644
--- a/src/db/utils.rs
+++ b/src/db/utils.rs
@@ -1,8 +1,7 @@
-use crate::model::report::Report;
-use tokio_rusqlite::Row;
-
pub fn format_key_for_search(value: &str) -> String {
- format!("replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower({}), 'à', 'a'), 'â', 'a'), 'ç', 'c'), 'è', 'e'), 'é', 'e'), 'ê', 'e'), 'ë', 'e'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ù', 'u'), 'û', 'u'), 'ü', 'u')", value)
+ // Lower doesn’t work on accentuated letters, hence the need to remove manually accents for
+ // uppercase letters as well.
+ format!("replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower({}), 'à', 'a'), 'â', 'a'), 'ç', 'c'), 'è', 'e'), 'é', 'e'), 'ê', 'e'), 'ë', 'e'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ù', 'u'), 'û', 'u'), 'ü', 'u'), 'À', 'A'), 'Â', 'A'), 'Ç', 'C'), 'È', 'E'), 'É', 'E'), 'Ê', 'E'), 'Ë', 'E'), 'Î', 'I'), 'Ï', 'I'), 'Ô', 'O'), 'Ù', 'U'), 'Û', 'U'), 'Ü', 'U')", value)
}
pub fn one<A, I: Iterator<Item = Result<A, rusqlite::Error>>>(
@@ -16,12 +15,3 @@ pub fn one<A, I: Iterator<Item = Result<A, rusqlite::Error>>>(
)),
}
}
-
-pub fn row_to_report(row: &Row) -> Result<Report, rusqlite::Error> {
- Ok(Report {
- date: row.get(0)?,
- name: row.get(1)?,
- amount: row.get(2)?,
- action: row.get(3)?,
- })
-}
diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs
index 3bfca71..0a903c4 100644
--- a/src/jobs/mod.rs
+++ b/src/jobs/mod.rs
@@ -1,25 +1,11 @@
-mod weekly_report;
-
use tokio::time::{sleep, Duration};
use tokio_rusqlite::Connection;
use crate::db;
-use crate::model::config::Config;
use crate::model::job::Job;
-pub async fn start(
- config: Config,
- db_conn: Connection,
- templates: minijinja::Environment<'_>,
-) {
+pub async fn start(db_conn: Connection) {
loop {
- if db::jobs::should_run(&db_conn, Job::WeeklyReport).await {
- log::info!("Starting weekly report job");
- if weekly_report::send(&config, &db_conn, &templates).await {
- db::jobs::actualize_last_execution(&db_conn, Job::WeeklyReport)
- .await;
- }
- }
if db::jobs::should_run(&db_conn, Job::MonthlyPayment).await {
log::info!("Starting monthly payment job");
db::payments::create_monthly_payments(&db_conn).await;
diff --git a/src/jobs/weekly_report.rs b/src/jobs/weekly_report.rs
deleted file mode 100644
index a91a3fb..0000000
--- a/src/jobs/weekly_report.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-use std::collections::HashMap;
-use tokio_rusqlite::Connection;
-
-use crate::db;
-use crate::mail;
-use crate::model::config::Config;
-use crate::payer;
-
-pub async fn send(
- config: &Config,
- db_conn: &Connection,
- env: &minijinja::Environment<'_>,
-) -> bool {
- match get_weekly_report(db_conn, env).await {
- Ok(report) => {
- let users = db::users::list(db_conn).await;
- mail::send(
- config,
- users
- .into_iter()
- .map(|u| mail::Recipient {
- name: u.name,
- address: u.email,
- })
- .collect(),
- "Rapport hebdomadaire".to_string(),
- report,
- )
- .await
- }
- Err(err) => {
- log::error!(
- "Error preparing weekly report from template: {:?}",
- err
- );
- false
- }
- }
-}
-
-async fn get_weekly_report(
- db_conn: &Connection,
- env: &minijinja::Environment<'_>,
-) -> Result<String, minijinja::Error> {
- let users = db::users::list(db_conn).await;
- let incomes_from = db::incomes::defined_for_all(db_conn).await;
- let user_incomes = match incomes_from {
- Some(from) => db::incomes::cumulative(db_conn, from).await,
- None => HashMap::new(),
- };
- let user_payments = db::payments::repartition(db_conn).await;
- let exceeding_payers =
- payer::exceeding(&users, &user_incomes, &user_payments);
-
- let last_week_payments = db::payments::last_week(db_conn).await;
- let last_week_incomes = db::incomes::last_week(db_conn).await;
-
- let template = env.get_template("report/report.j2")?;
- template.render(minijinja::context!(
- name => "John",
- exceeding_payers => exceeding_payers,
- payments => last_week_payments,
- incomes => last_week_incomes
- ))
-}
diff --git a/src/mail.rs b/src/mail.rs
deleted file mode 100644
index b6db0cd..0000000
--- a/src/mail.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-use chrono::Utc;
-use std::io::{Error, ErrorKind};
-use std::process::{Output, Stdio};
-use tokio::io::AsyncWriteExt;
-use tokio::process::Command;
-
-use crate::model::config::Config;
-
-static FROM_NAME: &str = "Budget";
-static FROM_ADDRESS: &str = "budget@guyonvarch.me";
-
-#[derive(Clone)]
-pub struct Recipient {
- pub name: String,
- pub address: String,
-}
-
-pub async fn send(
- config: &Config,
- recipients: Vec<Recipient>,
- subject: String,
- message: String,
-) -> bool {
- let headers = format_headers(recipients.clone(), subject);
-
- log::info!(
- "Sending mail{}\n{}",
- if config.mock_mails { " (MOCK)" } else { "" },
- headers.clone()
- );
-
- if config.mock_mails {
- true
- } else {
- let recipient_addresses = recipients
- .clone()
- .into_iter()
- .map(|r| r.address)
- .collect::<Vec<String>>();
-
- // https://github.com/NixOS/nixpkgs/issues/90248
- let mut command = Command::new("/run/wrappers/bin/sendmail");
- command.kill_on_drop(true);
- command.arg("-f").arg(FROM_ADDRESS);
- command.arg("--").args(recipient_addresses);
- command
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped());
-
- let message = format!("{}\n\n{}", headers, message);
- match spawn(command, &message.into_bytes()).await {
- Ok(output) => {
- if output.status.success() {
- log::info!("Mail sent");
- true
- } else {
- match String::from_utf8(output.stderr) {
- Ok(error) => {
- log::error!("Error sending email: {}", error)
- }
- _ => log::error!("Error sending email"),
- };
- false
- }
- }
- Err(err) => {
- log::error!("Error spawning command: {:?}", err);
- false
- }
- }
- }
-}
-
-fn format_headers(recipients: Vec<Recipient>, subject: String) -> String {
- let recipients = recipients
- .into_iter()
- .map(|r| format_address(r.name, r.address))
- .collect::<Vec<String>>()
- .join(", ");
-
- format!(
- "Date: {}\nFrom: {}\nTo: {}\nSubject: {}",
- Utc::now().to_rfc2822(),
- format_address(FROM_NAME.to_string(), FROM_ADDRESS.to_string()),
- recipients,
- subject,
- )
-}
-
-fn format_address(name: String, address: String) -> String {
- format!("{} <{}>", name, address)
-}
-
-async fn spawn(mut command: Command, stdin: &[u8]) -> Result<Output, Error> {
- let mut process = command.spawn()?;
- process
- .stdin
- .as_mut()
- .ok_or(Error::new(ErrorKind::Other, "Getting mutable stdin"))?
- .write_all(stdin)
- .await?;
- process.wait_with_output().await
-}
diff --git a/src/main.rs b/src/main.rs
index 0786f46..70bac81 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,15 +1,14 @@
+use anyhow::Result;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
-use tokio_rusqlite::Connection;
mod assets;
mod controller;
mod crypto;
mod db;
mod jobs;
-mod mail;
mod model;
mod payer;
mod queries;
@@ -21,27 +20,19 @@ mod validation;
use model::config;
#[tokio::main]
-async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+async fn main() -> Result<()> {
env_logger::init();
let config = config::from_env()
.unwrap_or_else(|err| panic!("Error reading config: {err}"));
- let db_conn = Connection::open(config.db_path.clone())
- .await
- .unwrap_or_else(|_| {
- panic!("Error while openning DB: {}", config.db_path)
- });
+ let db_conn = db::init(&config.db_path).await?;
let assets = assets::get();
let templates = templates::get()?;
- tokio::spawn(jobs::start(
- config.clone(),
- db_conn.clone(),
- templates.clone(),
- ));
+ tokio::spawn(jobs::start(db_conn.clone()));
let listener = TcpListener::bind(config.socket_address).await?;
log::info!("Starting server at {}", config.socket_address);
diff --git a/src/model/config.rs b/src/model/config.rs
index 1fa5bb4..f40b0fb 100644
--- a/src/model/config.rs
+++ b/src/model/config.rs
@@ -6,7 +6,6 @@ use std::str::FromStr;
pub struct Config {
pub auth_secret: String,
pub db_path: String,
- pub mock_mails: bool,
pub secure_cookies: bool,
pub socket_address: SocketAddr,
}
@@ -15,7 +14,6 @@ pub fn from_env() -> Result<Config, String> {
Ok(Config {
auth_secret: read_string("AUTH_SECRET")?,
db_path: read_string("DB_PATH")?,
- mock_mails: read_bool("MOCK_MAILS")?,
secure_cookies: read_bool("SECURE_COOKIES")?,
socket_address: read_socket_address("SOCKET_ADDRESS")?,
})
diff --git a/src/model/job.rs b/src/model/job.rs
index f31cfa0..b10b2df 100644
--- a/src/model/job.rs
+++ b/src/model/job.rs
@@ -3,7 +3,6 @@ use std::fmt;
#[derive(Debug)]
pub enum Job {
MonthlyPayment,
- WeeklyReport,
}
impl fmt::Display for Job {
diff --git a/src/model/mod.rs b/src/model/mod.rs
index fb07721..55adadd 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -5,5 +5,4 @@ pub mod income;
pub mod job;
pub mod login;
pub mod payment;
-pub mod report;
pub mod user;
diff --git a/src/model/report.rs b/src/model/report.rs
deleted file mode 100644
index e944745..0000000
--- a/src/model/report.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
-use std::fmt;
-
-#[derive(Debug, serde::Serialize)]
-pub struct Report {
- pub date: String,
- pub name: String,
- pub amount: i64,
- pub action: Action,
-}
-
-#[derive(Debug, PartialEq, serde::Serialize)]
-pub enum Action {
- Created,
- Updated,
- Deleted,
-}
-
-impl fmt::Display for Action {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{:?}", self)
- }
-}
-
-impl FromSql for Action {
- fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
- match value {
- ValueRef::Text(text) => match std::str::from_utf8(text) {
- Ok("Created") => Ok(Action::Created),
- Ok("Updated") => Ok(Action::Updated),
- Ok("Deleted") => Ok(Action::Deleted),
- Ok(str) => Err(FromSqlError::Other(
- format!("Unknown action: {str}").into(),
- )),
- Err(err) => Err(FromSqlError::Other(err.into())),
- },
- _ => Err(FromSqlError::InvalidType),
- }
- }
-}
diff --git a/src/routes.rs b/src/routes.rs
index aca4284..7107a60 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -28,7 +28,7 @@ pub async fn routes(
let response = match (method, path) {
(&Method::HEAD, ["status"]) => controller::utils::ok(),
- (&Method::GET, ["status"]) => controller::utils::text("ok".to_string()),
+ (&Method::GET, ["status"]) => controller::utils::text("ok"),
(&Method::GET, ["login"]) => {
controller::login::page(&assets, &templates, None).await
}
diff --git a/src/templates.rs b/src/templates.rs
index 8f160dc..f6f4e62 100644
--- a/src/templates.rs
+++ b/src/templates.rs
@@ -1,3 +1,4 @@
+use anyhow::{Error, Result};
use std::fs;
use crate::queries;
@@ -11,21 +12,26 @@ pub enum Header {
Statistics,
}
-pub fn get() -> Result<minijinja::Environment<'static>, String> {
+pub fn get() -> Result<minijinja::Environment<'static>> {
let mut env = minijinja::Environment::new();
for path in read_files_recursive("templates") {
let path = path
.to_str()
- .ok_or("Error getting string of path: {path:?}")?
+ .ok_or(Error::msg("Error getting string of path: {path:?}"))?
.to_string();
- let content = fs::read_to_string(&path)
- .map_err(|_| "Error reading template {path}")?;
+ let content = fs::read_to_string(&path).map_err(|err| {
+ Error::msg(format!("Error reading template {path}: {err}"))
+ })?;
let path_without_prefix = path
.strip_prefix("templates/")
- .ok_or("Error removing prefix from template path")?
+ .ok_or(Error::msg("Error removing prefix from template path"))?
.to_string();
env.add_template_owned(path_without_prefix, content)
- .map_err(|_| "Error adding template {path} to environment")?;
+ .map_err(|err| {
+ Error::msg(format!(
+ "Error adding template {path} to environment: {err}"
+ ))
+ })?;
}
env.add_function("payments_params", payments_params);
@@ -92,11 +98,11 @@ fn numeric(n: i64) -> String {
format!("{}{}", sign, str)
}
-fn pluralize(n: i32, s: String) -> String {
+fn pluralize(n: i32, s: &str) -> String {
if n > 0 {
format!("{s}s")
} else {
- s
+ s.to_string()
}
}
@@ -104,7 +110,7 @@ fn round(n: f32) -> i32 {
n.round() as i32
}
-fn with_param(url: String, key: String, value: String) -> String {
+fn with_param(url: &str, key: &str, value: String) -> String {
if url.contains("?") {
format!("{url}&{key}={value}")
} else {
@@ -130,8 +136,8 @@ fn filter(
res
}
-fn rgrouped(str: String, n: usize) -> Vec<String> {
- let mut str = str;
+fn rgrouped(str: impl Into<String>, n: usize) -> Vec<String> {
+ let mut str = str.into();
let mut l = str.len();
let mut res = vec![];
while l > n {
diff --git a/src/utils/cookie.rs b/src/utils/cookie.rs
index 826efa9..e21e7d4 100644
--- a/src/utils/cookie.rs
+++ b/src/utils/cookie.rs
@@ -1,5 +1,5 @@
use hex;
-use rand_core::{OsRng, RngCore};
+use rand_core::{OsRng, TryRngCore};
use crate::crypto::signed;
use crate::model::config::Config;
@@ -8,7 +8,7 @@ const TOKEN_BYTES: usize = 20;
pub fn login(config: &Config, token: &str) -> Result<String, String> {
let signed_token = signed::sign(&config.auth_secret, token)?;
- Ok(cookie(config, &signed_token, 24 * 60 * 60))
+ Ok(cookie(config, &signed_token, 365 * 24 * 60 * 60))
}
pub fn logout(config: &Config) -> String {
@@ -22,10 +22,12 @@ pub fn extract_token(config: &Config, cookie: &str) -> Result<String, String> {
signed::verify(&config.auth_secret, signed_cookie)
}
-pub fn generate_token() -> String {
+pub fn generate_token() -> Result<String, String> {
let mut token = [0u8; TOKEN_BYTES];
- OsRng.fill_bytes(&mut token);
- hex::encode(token)
+ OsRng
+ .try_fill_bytes(&mut token)
+ .map_err(|_| "Error generating random token")?;
+ Ok(hex::encode(token))
}
fn cookie(config: &Config, token: &str, max_age_seconds: i32) -> String {