diff --git a/.gitignore b/.gitignore
index ea8c4bf..fabfb87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+/config.toml
diff --git a/2026-01-12_16:52.patch b/2026-01-12_16:52.patch
deleted file mode 100644
index d4e4bee..0000000
--- a/2026-01-12_16:52.patch
+++ /dev/null
@@ -1,67 +0,0 @@
-diff --git a/src/web/mod.rs b/src/web/mod.rs
-index f919e6e..4f03aa9 100644
---- a/src/web/mod.rs
-+++ b/src/web/mod.rs
-@@ -11,6 +11,7 @@ pub async fn start() -> anyhow::Result<()> {
- let addr = "0.0.0.0:3000";
- let app = Router::new()
- .route("/", get(pages::home::get_page))
-+ .route("/login", get(pages::login::get_page))
- .nest_service(
- "/static",
- ServiceBuilder::new()
-diff --git a/src/web/pages/mod.rs b/src/web/pages/mod.rs
-index 0c1f8ad..d4fc7a0 100644
---- a/src/web/pages/mod.rs
-+++ b/src/web/pages/mod.rs
-@@ -1,6 +1,8 @@
-
-
-+
- pub mod home;
-+pub mod login;
- pub mod error;
-
-
-diff --git a/templates/base.html b/templates/base.html
-index 6731e87..86574ca 100644
---- a/templates/base.html
-+++ b/templates/base.html
-@@ -3,6 +3,7 @@
-
-
- {{ self.title() }}
-+
-
-
- {% include "header.html" %}
-diff --git a/templates/header.html b/templates/header.html
-index c247db4..cf4fb71 100644
---- a/templates/header.html
-+++ b/templates/header.html
-@@ -1,6 +1,25 @@
-
diff --git a/2026-01-12_16:55.patch b/2026-01-12_16:55.patch
deleted file mode 100644
index d4e4bee..0000000
--- a/2026-01-12_16:55.patch
+++ /dev/null
@@ -1,67 +0,0 @@
-diff --git a/src/web/mod.rs b/src/web/mod.rs
-index f919e6e..4f03aa9 100644
---- a/src/web/mod.rs
-+++ b/src/web/mod.rs
-@@ -11,6 +11,7 @@ pub async fn start() -> anyhow::Result<()> {
- let addr = "0.0.0.0:3000";
- let app = Router::new()
- .route("/", get(pages::home::get_page))
-+ .route("/login", get(pages::login::get_page))
- .nest_service(
- "/static",
- ServiceBuilder::new()
-diff --git a/src/web/pages/mod.rs b/src/web/pages/mod.rs
-index 0c1f8ad..d4fc7a0 100644
---- a/src/web/pages/mod.rs
-+++ b/src/web/pages/mod.rs
-@@ -1,6 +1,8 @@
-
-
-+
- pub mod home;
-+pub mod login;
- pub mod error;
-
-
-diff --git a/templates/base.html b/templates/base.html
-index 6731e87..86574ca 100644
---- a/templates/base.html
-+++ b/templates/base.html
-@@ -3,6 +3,7 @@
-
-
- {{ self.title() }}
-+
-
-
- {% include "header.html" %}
-diff --git a/templates/header.html b/templates/header.html
-index c247db4..cf4fb71 100644
---- a/templates/header.html
-+++ b/templates/header.html
-@@ -1,6 +1,25 @@
-
diff --git a/Cargo.lock b/Cargo.lock
index 2aa9463..901c20a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -300,6 +300,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+
[[package]]
name = "cc"
version = "1.2.52"
@@ -325,6 +331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
+ "clap_derive",
]
[[package]]
@@ -339,6 +346,18 @@ dependencies = [
"strsim",
]
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "clap_lex"
version = "0.7.6"
@@ -399,12 +418,107 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "darling"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+[[package]]
+name = "deranged"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "diesel"
+version = "2.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e130c806dccc85428c564f2dc5a96e05b6615a27c9a28776bd7761a9af4bb552"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "diesel_derives",
+ "downcast-rs",
+ "ipnet",
+ "itoa",
+ "libc",
+ "pq-sys",
+ "time",
+ "uuid",
+]
+
+[[package]]
+name = "diesel_derives"
+version = "2.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c30b2969f923fa1f73744b92bb7df60b858df8832742d9a3aceb79236c0be1d2"
+dependencies = [
+ "diesel_table_macro_syntax",
+ "dsl_auto_type",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "diesel_migrations"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "745fd255645f0f1135f9ec55c7b00e0882192af9683ab4731e4bba3da82b8f9c"
+dependencies = [
+ "diesel",
+ "migrations_internals",
+ "migrations_macros",
+]
+
+[[package]]
+name = "diesel_table_macro_syntax"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c"
+dependencies = [
+ "syn",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -415,6 +529,43 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
+
+[[package]]
+name = "dsl_auto_type"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e"
+dependencies = [
+ "darling",
+ "either",
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
[[package]]
name = "env_filter"
version = "0.1.4"
@@ -461,23 +612,18 @@ dependencies = [
]
[[package]]
-name = "form_urlencoded"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
-dependencies = [
- "percent-encoding",
-]
-
-[[package]]
-name = "fuck_microsoft_access"
+name = "fma"
version = "0.1.0"
dependencies = [
"anyhow",
"askama",
"axum",
+ "camino",
"clap",
+ "diesel",
+ "diesel_migrations",
"env_logger",
+ "ipnet",
"log",
"serde",
"serde_json",
@@ -485,6 +631,22 @@ dependencies = [
"toml",
"tower",
"tower-http",
+ "url",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
]
[[package]]
@@ -566,6 +728,12 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
[[package]]
name = "http"
version = "1.4.0"
@@ -654,6 +822,114 @@ dependencies = [
"tower-service",
]
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -664,6 +940,12 @@ dependencies = [
"hashbrown",
]
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
[[package]]
name = "iri-string"
version = "0.7.10"
@@ -736,6 +1018,12 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
[[package]]
name = "log"
version = "0.4.29"
@@ -754,6 +1042,27 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+[[package]]
+name = "migrations_internals"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d"
+dependencies = [
+ "serde",
+ "toml",
+]
+
+[[package]]
+name = "migrations_macros"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703"
+dependencies = [
+ "migrations_internals",
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "mime"
version = "0.3.17"
@@ -791,6 +1100,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -851,6 +1166,21 @@ dependencies = [
"portable-atomic",
]
+[[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -860,6 +1190,17 @@ dependencies = [
"zerocopy",
]
+[[package]]
+name = "pq-sys"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61"
+dependencies = [
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.105"
@@ -1080,6 +1421,12 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
[[package]]
name = "strsim"
version = "0.11.1"
@@ -1103,6 +1450,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "thiserror"
version = "2.0.17"
@@ -1123,6 +1481,46 @@ dependencies = [
"syn",
]
+[[package]]
+name = "time"
+version = "0.3.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
[[package]]
name = "tokio"
version = "1.49.0"
@@ -1331,12 +1729,31 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -1354,6 +1771,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
[[package]]
name = "version_check"
version = "0.9.5"
@@ -1524,6 +1947,35 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
[[package]]
name = "zerocopy"
version = "0.8.33"
@@ -1544,6 +1996,60 @@ dependencies = [
"syn",
]
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "zmij"
version = "1.0.13"
diff --git a/Cargo.toml b/Cargo.toml
index 02e16e0..aa636da 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "fuck_microsoft_access"
+name = "fma"
version = "0.1.0"
edition = "2024"
@@ -7,8 +7,12 @@ edition = "2024"
anyhow = "1.0.100"
askama = "0.15.1"
axum = { version = "0.8.8", features = ["macros", "ws"] }
-clap = "4.5.54"
+camino = "1.2.2"
+clap = { version = "4.5.54", features = ["derive"] }
+diesel = { version = "2.3.5", features = ["uuid", "time", "postgres", "ipnet-address"] }
+diesel_migrations = { version = "2.3.1", features = ["postgres"] }
env_logger = "0.11.8"
+ipnet = "2.11.0"
log = "0.4.29"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
@@ -16,3 +20,4 @@ tokio = { version = "1.49.0", features = ["io-util", "macros", "net", "rt-multi-
toml = "0.9.11"
tower = { version = "0.5.3", features = ["full"] }
tower-http = { version = "0.6.8", features = ["full"] }
+url = { version = "2.5.8", features = ["serde"] }
diff --git a/diesel.toml b/diesel.toml
new file mode 100644
index 0000000..bb1d1f7
--- /dev/null
+++ b/diesel.toml
@@ -0,0 +1,9 @@
+# For documentation on how to configure this file,
+# see https://diesel.rs/guides/configuring-diesel-cli
+
+[print_schema]
+file = "src/db/schema.rs"
+custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
+
+[migrations_directory]
+dir = "migrations"
diff --git a/migrations/.diesel_lock b/migrations/.diesel_lock
new file mode 100644
index 0000000..e69de29
diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql
new file mode 100644
index 0000000..a9f5260
--- /dev/null
+++ b/migrations/00000000000000_diesel_initial_setup/down.sql
@@ -0,0 +1,6 @@
+-- This file was automatically created by Diesel to setup helper functions
+-- and other internal bookkeeping. This file is safe to edit, any future
+-- changes will be added to existing projects as new migrations.
+
+DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
+DROP FUNCTION IF EXISTS diesel_set_updated_at();
diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql
new file mode 100644
index 0000000..d68895b
--- /dev/null
+++ b/migrations/00000000000000_diesel_initial_setup/up.sql
@@ -0,0 +1,36 @@
+-- This file was automatically created by Diesel to setup helper functions
+-- and other internal bookkeeping. This file is safe to edit, any future
+-- changes will be added to existing projects as new migrations.
+
+
+
+
+-- Sets up a trigger for the given table to automatically set a column called
+-- `updated_at` whenever the row is modified (unless `updated_at` was included
+-- in the modified columns)
+--
+-- # Example
+--
+-- ```sql
+-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
+--
+-- SELECT diesel_manage_updated_at('users');
+-- ```
+CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
+BEGIN
+ EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
+ FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
+BEGIN
+ IF (
+ NEW IS DISTINCT FROM OLD AND
+ NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
+ ) THEN
+ NEW.updated_at := current_timestamp;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
diff --git a/migrations/2026-01-13-095817-0000_users/down.sql b/migrations/2026-01-13-095817-0000_users/down.sql
new file mode 100644
index 0000000..c99ddcd
--- /dev/null
+++ b/migrations/2026-01-13-095817-0000_users/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS users;
diff --git a/migrations/2026-01-13-095817-0000_users/up.sql b/migrations/2026-01-13-095817-0000_users/up.sql
new file mode 100644
index 0000000..3cb8c4a
--- /dev/null
+++ b/migrations/2026-01-13-095817-0000_users/up.sql
@@ -0,0 +1,18 @@
+CREATE TABLE IF NOT EXISTS users (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ username TEXT NOT NULL UNIQUE,
+ email TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ password_salt TEXT NOT NULL,
+ first_name TEXT NOT NULL,
+ last_name TEXT NOT NULL,
+ display_name TEXT,
+ date_of_birth DATE,
+ phone_number TEXT,
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ last_login_at TIMESTAMPTZ,
+
+ -- u128 bitfield for permissions
+ permissions NUMERIC(39,0) NOT NULL DEFAULT 0
+)
diff --git a/migrations/2026-01-13-095838-0000_clients/down.sql b/migrations/2026-01-13-095838-0000_clients/down.sql
new file mode 100644
index 0000000..1100f5d
--- /dev/null
+++ b/migrations/2026-01-13-095838-0000_clients/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS clients;
diff --git a/migrations/2026-01-13-095838-0000_clients/up.sql b/migrations/2026-01-13-095838-0000_clients/up.sql
new file mode 100644
index 0000000..b2ed366
--- /dev/null
+++ b/migrations/2026-01-13-095838-0000_clients/up.sql
@@ -0,0 +1,21 @@
+CREATE TABLE IF NOT EXISTS clients (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+
+ first_name TEXT NOT NULL,
+ last_name TEXT NOT NULL,
+
+ date_of_birth DATE,
+ phone_number TEXT,
+ gov_id_number TEXT,
+ house_number TEXT,
+ address_line TEXT,
+ city TEXT,
+ state TEXT,
+ postal_code TEXT,
+ country TEXT,
+
+ worker_user_id BIGINT,
+
+ CONSTRAINT fk_worker_user_id FOREIGN KEY (worker_user_id) REFERENCES users(id) ON DELETE CASCADE
+)
diff --git a/migrations/2026-01-13-095842-0000_warehouses/down.sql b/migrations/2026-01-13-095842-0000_warehouses/down.sql
new file mode 100644
index 0000000..548e385
--- /dev/null
+++ b/migrations/2026-01-13-095842-0000_warehouses/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS warehouses;
diff --git a/migrations/2026-01-13-095842-0000_warehouses/up.sql b/migrations/2026-01-13-095842-0000_warehouses/up.sql
new file mode 100644
index 0000000..19c8692
--- /dev/null
+++ b/migrations/2026-01-13-095842-0000_warehouses/up.sql
@@ -0,0 +1,5 @@
+CREATE TABLE IF NOT EXISTS warehouses (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+)
diff --git a/migrations/2026-01-13-095843-0000_inventory_catalog/down.sql b/migrations/2026-01-13-095843-0000_inventory_catalog/down.sql
new file mode 100644
index 0000000..8f5ac6c
--- /dev/null
+++ b/migrations/2026-01-13-095843-0000_inventory_catalog/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS inventory_catalog;
diff --git a/migrations/2026-01-13-095843-0000_inventory_catalog/up.sql b/migrations/2026-01-13-095843-0000_inventory_catalog/up.sql
new file mode 100644
index 0000000..4c77ca4
--- /dev/null
+++ b/migrations/2026-01-13-095843-0000_inventory_catalog/up.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS inventory_catalog (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name TEXT NOT NULL,
+ description TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+)
diff --git a/migrations/2026-01-13-095844-0000_inventory/down.sql b/migrations/2026-01-13-095844-0000_inventory/down.sql
new file mode 100644
index 0000000..c81108c
--- /dev/null
+++ b/migrations/2026-01-13-095844-0000_inventory/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS inventory;
diff --git a/migrations/2026-01-13-095844-0000_inventory/up.sql b/migrations/2026-01-13-095844-0000_inventory/up.sql
new file mode 100644
index 0000000..2ec7512
--- /dev/null
+++ b/migrations/2026-01-13-095844-0000_inventory/up.sql
@@ -0,0 +1,11 @@
+CREATE TABLE IF NOT EXISTS inventory (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ warehouse_id BIGINT NOT NULL,
+ catalog_id BIGINT NOT NULL,
+ count BIGINT NOT NULL DEFAULT 0,
+
+ UNIQUE (warehouse_id, catalog_id),
+
+ CONSTRAINT fk_warehouse_id FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE CASCADE,
+ CONSTRAINT fk_catalog_id FOREIGN KEY (catalog_id) REFERENCES inventory_catalog(id) ON DELETE CASCADE
+)
diff --git a/migrations/2026-01-13-095855-0000_tickets/down.sql b/migrations/2026-01-13-095855-0000_tickets/down.sql
new file mode 100644
index 0000000..7d4d917
--- /dev/null
+++ b/migrations/2026-01-13-095855-0000_tickets/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS tickets;
diff --git a/migrations/2026-01-13-095855-0000_tickets/up.sql b/migrations/2026-01-13-095855-0000_tickets/up.sql
new file mode 100644
index 0000000..c5053d7
--- /dev/null
+++ b/migrations/2026-01-13-095855-0000_tickets/up.sql
@@ -0,0 +1,3 @@
+CREATE TABLE IF NOT EXISTS tickets (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
+)
diff --git a/migrations/2026-01-13-101555-0000_invoices/down.sql b/migrations/2026-01-13-101555-0000_invoices/down.sql
new file mode 100644
index 0000000..b30b8a4
--- /dev/null
+++ b/migrations/2026-01-13-101555-0000_invoices/down.sql
@@ -0,0 +1,2 @@
+DROP TABLE IF EXISTS invoices;
+
diff --git a/migrations/2026-01-13-101555-0000_invoices/up.sql b/migrations/2026-01-13-101555-0000_invoices/up.sql
new file mode 100644
index 0000000..2bd97d9
--- /dev/null
+++ b/migrations/2026-01-13-101555-0000_invoices/up.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS invoices (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ client_id BIGINT NOT NULL,
+ amount REAL NOT NULL, -- f32
+
+ CONSTRAINT fk_client_id FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE
+)
diff --git a/migrations/2026-01-13-101601-0000_services/down.sql b/migrations/2026-01-13-101601-0000_services/down.sql
new file mode 100644
index 0000000..49ef0d0
--- /dev/null
+++ b/migrations/2026-01-13-101601-0000_services/down.sql
@@ -0,0 +1,2 @@
+DROP TABLE IF EXISTS services;
+
diff --git a/migrations/2026-01-13-101601-0000_services/up.sql b/migrations/2026-01-13-101601-0000_services/up.sql
new file mode 100644
index 0000000..af48d55
--- /dev/null
+++ b/migrations/2026-01-13-101601-0000_services/up.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS services (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name TEXT NOT NULL,
+ client_id BIGINT NOT NULL,
+
+ CONSTRAINT fk_client_id FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE
+)
diff --git a/migrations/2026-01-13-103156-0000_assigned_warehouse_managers/down.sql b/migrations/2026-01-13-103156-0000_assigned_warehouse_managers/down.sql
new file mode 100644
index 0000000..10c044e
--- /dev/null
+++ b/migrations/2026-01-13-103156-0000_assigned_warehouse_managers/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS assigned_warehouse_managers;
diff --git a/migrations/2026-01-13-103156-0000_assigned_warehouse_managers/up.sql b/migrations/2026-01-13-103156-0000_assigned_warehouse_managers/up.sql
new file mode 100644
index 0000000..a572fe4
--- /dev/null
+++ b/migrations/2026-01-13-103156-0000_assigned_warehouse_managers/up.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS assigned_warehouse_managers (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ warehouse_id BIGINT NOT NULL,
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+
+ CONSTRAINT fk_warehouse_id FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE CASCADE,
+ CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+)
diff --git a/migrations/2026-01-13-103443-0000_warehouse_actions/down.sql b/migrations/2026-01-13-103443-0000_warehouse_actions/down.sql
new file mode 100644
index 0000000..e8cbd41
--- /dev/null
+++ b/migrations/2026-01-13-103443-0000_warehouse_actions/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS warehouse_actions;
diff --git a/migrations/2026-01-13-103443-0000_warehouse_actions/up.sql b/migrations/2026-01-13-103443-0000_warehouse_actions/up.sql
new file mode 100644
index 0000000..a3bc127
--- /dev/null
+++ b/migrations/2026-01-13-103443-0000_warehouse_actions/up.sql
@@ -0,0 +1,11 @@
+CREATE TABLE IF NOT EXISTS warehouse_actions (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ warehouse_id BIGINT NOT NULL,
+ count BIGINT NOT NULL,
+ reason TEXT NOT NULL,
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
+
+ CONSTRAINT fk_warehouse_id FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE CASCADE,
+ CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+)
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..e35a857
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,89 @@
+use anyhow::bail;
+use clap::Parser;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, clap::Parser)]
+pub struct CliArgs {
+
+ /// Path to config file
+ #[arg(long="config", short='C', default_value="./config.toml")]
+ config_path: camino::Utf8PathBuf,
+
+ #[arg(long="host", short='H')]
+ host: Option,
+ #[arg(long="port", short='p')]
+ port: Option,
+ #[arg(long="database", short='D')]
+ database: Option,
+}
+
+#[derive(Debug, Clone, Default, Deserialize, Serialize)]
+pub struct Config {
+ pub web: ConfigWeb,
+ pub database: ConfigDb
+}
+
+#[derive(Debug, Clone, Default, Deserialize, Serialize)]
+pub struct ConfigWeb {
+ pub host: String,
+ pub port: u16,
+}
+
+#[derive(Debug, Clone, Default, Deserialize, Serialize)]
+pub struct ConfigDb {
+ host: String,
+ port: u16,
+ username: String,
+ password: String,
+ database: String,
+}
+
+
+impl Config {
+ pub fn parse() -> anyhow::Result {
+ let cli = CliArgs::parse();
+ let mut cfg = Self::default();
+ let mut is_new = true;
+ if cli.config_path.exists() {
+ log::info!("Config file exists, reading");
+ let cfg_s = std::fs::read_to_string(&cli.config_path)?;
+ cfg = toml::from_str(&cfg_s)?;
+ is_new = false;
+ }
+ cfg.web.host = cli.host.unwrap_or("0.0.0.0".to_string());
+ cfg.web.port = cli.port.unwrap_or(3000);
+ if let Some(db) = cli.database {
+ cfg.database.host = db.host_str().unwrap_or("0.0.0.0").to_string();
+ cfg.database.port = db.port().unwrap_or(5432);
+ cfg.database.username = db.username().to_string();
+ cfg.database.password = db.password().unwrap_or("").to_string();
+ cfg.database.database = {
+ match db.path_segments() {
+ Some(mut seg) => seg.next().unwrap_or("").to_string(),
+ None => String::new()
+ }
+ }
+ }
+
+ if is_new {
+ log::info!("Config file doesnt exist, creating new");
+ let content = toml::to_string_pretty(&cfg)?;
+ std::fs::write(&cli.config_path, content)?;
+ }
+ Ok(cfg)
+ }
+
+ pub fn database_url(&self) -> anyhow::Result {
+ let mut u = url::Url::parse("postgres://0.0.0.0/")?;
+ u.set_host(Some(&self.database.host))?;
+ let _ = u.set_port(Some(self.database.port));
+ let _ = u.set_username(&self.database.username);
+ if self.database.password.is_empty() {
+ let _ = u.set_password(None);
+ } else {
+ let _ = u.set_password(Some(&self.database.password));
+ }
+ u.set_path(&self.database.database);
+ Ok(u)
+ }
+}
diff --git a/src/db/mod.rs b/src/db/mod.rs
new file mode 100644
index 0000000..a6a27b2
--- /dev/null
+++ b/src/db/mod.rs
@@ -0,0 +1,19 @@
+use diesel::{Connection, PgConnection};
+use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
+
+pub mod schema;
+
+pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
+
+fn run_migrations(conn: &mut impl MigrationHarness) {
+ conn.run_pending_migrations(MIGRATIONS).unwrap();
+ log::info!("Running migrations");
+}
+
+
+pub fn start(cfg: &crate::config::Config) -> anyhow::Result {
+ let mut connection = PgConnection::establish(&cfg.database_url()?.to_string())?;
+ run_migrations(&mut connection);
+ Ok(connection)
+}
+
diff --git a/src/db/schema.rs b/src/db/schema.rs
new file mode 100644
index 0000000..fc0768c
--- /dev/null
+++ b/src/db/schema.rs
@@ -0,0 +1,129 @@
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+ assigned_warehouse_managers (id) {
+ id -> Int8,
+ user_id -> Int8,
+ warehouse_id -> Int8,
+ assigned_at -> Timestamptz,
+ }
+}
+
+diesel::table! {
+ clients (id) {
+ id -> Int8,
+ email -> Text,
+ first_name -> Text,
+ last_name -> Text,
+ date_of_birth -> Nullable,
+ phone_number -> Nullable,
+ gov_id_number -> Nullable,
+ house_number -> Nullable,
+ address_line -> Nullable,
+ city -> Nullable,
+ state -> Nullable,
+ postal_code -> Nullable,
+ country -> Nullable,
+ worker_user_id -> Nullable,
+ }
+}
+
+diesel::table! {
+ inventory (id) {
+ id -> Int8,
+ warehouse_id -> Int8,
+ catalog_id -> Int8,
+ count -> Int8,
+ }
+}
+
+diesel::table! {
+ inventory_catalog (id) {
+ id -> Int8,
+ name -> Text,
+ description -> Nullable,
+ created_at -> Timestamptz,
+ }
+}
+
+diesel::table! {
+ invoices (id) {
+ id -> Int8,
+ client_id -> Int8,
+ amount -> Float4,
+ }
+}
+
+diesel::table! {
+ services (id) {
+ id -> Int8,
+ name -> Text,
+ client_id -> Int8,
+ }
+}
+
+diesel::table! {
+ tickets (id) {
+ id -> Int8,
+ }
+}
+
+diesel::table! {
+ users (id) {
+ id -> Int8,
+ username -> Text,
+ email -> Text,
+ password_hash -> Text,
+ password_salt -> Text,
+ first_name -> Text,
+ last_name -> Text,
+ display_name -> Nullable,
+ date_of_birth -> Nullable,
+ phone_number -> Nullable,
+ created_at -> Timestamptz,
+ last_login_at -> Nullable,
+ permissions -> Numeric,
+ }
+}
+
+diesel::table! {
+ warehouse_actions (id) {
+ id -> Int8,
+ user_id -> Int8,
+ warehouse_id -> Int8,
+ count -> Int8,
+ reason -> Text,
+ timestamp -> Timestamptz,
+ }
+}
+
+diesel::table! {
+ warehouses (id) {
+ id -> Int8,
+ name -> Text,
+ created_at -> Timestamptz,
+ }
+}
+
+diesel::joinable!(assigned_warehouse_managers -> users (user_id));
+diesel::joinable!(assigned_warehouse_managers -> warehouses (warehouse_id));
+diesel::joinable!(clients -> users (worker_user_id));
+diesel::joinable!(inventory -> inventory_catalog (catalog_id));
+diesel::joinable!(inventory -> warehouses (warehouse_id));
+diesel::joinable!(invoices -> clients (client_id));
+diesel::joinable!(services -> clients (client_id));
+diesel::joinable!(warehouse_actions -> users (user_id));
+diesel::joinable!(warehouse_actions -> warehouses (warehouse_id));
+
+diesel::allow_tables_to_appear_in_same_query!(
+ assigned_warehouse_managers,
+ clients,
+ inventory,
+ inventory_catalog,
+ invoices,
+ services,
+ tickets,
+ users,
+ warehouse_actions,
+ warehouses,
+);
diff --git a/src/main.rs b/src/main.rs
index 152dd05..6f840c6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,14 +1,24 @@
use log::LevelFilter;
mod web;
-
+mod db;
+mod config;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::builder()
- .filter_module("fuck_microsoft_access", LevelFilter::Debug)
+ .filter_module("fma", LevelFilter::Debug)
+ .filter_module("diesel_migrations", LevelFilter::Debug)
.init();
+ let cfg = config::Config::parse()?;
+
+ unsafe {
+ // SAFETY: This runs while there are no other threads running, which is required for this
+ // function to be safe
+ std::env::set_var("DATABASE_URL", cfg.database_url()?.to_string());
+ }
- web::start().await?;
+ let db = db::start(&cfg)?;
+ web::start(&cfg).await?;
Ok(())
}
diff --git a/src/schema.rs b/src/schema.rs
new file mode 100644
index 0000000..6077008
--- /dev/null
+++ b/src/schema.rs
@@ -0,0 +1,132 @@
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+ assigned_warehouse_managers (id) {
+ id -> Int8,
+ user_id -> Int8,
+ warehouse_id -> Int8,
+ assigned_at -> Timestamptz,
+ }
+}
+
+diesel::table! {
+ clients (id) {
+ id -> Int8,
+ email -> Text,
+ first_name -> Text,
+ last_name -> Text,
+ date_of_birth -> Nullable,
+ phone_number -> Nullable,
+ gov_id_number -> Nullable,
+ house_number -> Nullable,
+ address_line -> Nullable,
+ city -> Nullable,
+ state -> Nullable,
+ postal_code -> Nullable,
+ country -> Nullable,
+ worker_user_id -> Nullable,
+ }
+}
+
+diesel::table! {
+ inventory (id) {
+ id -> Int8,
+ warehouse_id -> Int8,
+ catalog_id -> Int8,
+ count -> Int8,
+ }
+}
+
+diesel::table! {
+ inventory_catalog (id) {
+ id -> Int4,
+ title -> Varchar,
+ body -> Text,
+ published -> Bool,
+ }
+}
+
+diesel::table! {
+ invoices (id) {
+ id -> Int8,
+ client_id -> Int8,
+ amount -> Float4,
+ }
+}
+
+diesel::table! {
+ services (id) {
+ id -> Int8,
+ name -> Text,
+ client_id -> Int8,
+ }
+}
+
+diesel::table! {
+ tickets (id) {
+ id -> Int4,
+ title -> Varchar,
+ body -> Text,
+ published -> Bool,
+ }
+}
+
+diesel::table! {
+ users (id) {
+ id -> Int8,
+ username -> Text,
+ email -> Text,
+ password_hash -> Text,
+ password_salt -> Text,
+ first_name -> Text,
+ last_name -> Text,
+ display_name -> Nullable,
+ date_of_birth -> Nullable,
+ phone_number -> Nullable,
+ created_at -> Timestamptz,
+ last_login_at -> Nullable,
+ permissions -> Numeric,
+ }
+}
+
+diesel::table! {
+ warehouse_actions (id) {
+ id -> Int8,
+ user_id -> Int8,
+ warehouse_id -> Int8,
+ count -> Int8,
+ reason -> Text,
+ timestamp -> Timestamptz,
+ }
+}
+
+diesel::table! {
+ warehouses (id) {
+ id -> Int8,
+ name -> Text,
+ created_at -> Timestamptz,
+ }
+}
+
+diesel::joinable!(assigned_warehouse_managers -> users (user_id));
+diesel::joinable!(assigned_warehouse_managers -> warehouses (warehouse_id));
+diesel::joinable!(clients -> users (worker_user_id));
+diesel::joinable!(inventory -> inventory_catalog (catalog_id));
+diesel::joinable!(inventory -> warehouses (warehouse_id));
+diesel::joinable!(invoices -> clients (client_id));
+diesel::joinable!(services -> clients (client_id));
+diesel::joinable!(warehouse_actions -> users (user_id));
+diesel::joinable!(warehouse_actions -> warehouses (warehouse_id));
+
+diesel::allow_tables_to_appear_in_same_query!(
+ assigned_warehouse_managers,
+ clients,
+ inventory,
+ inventory_catalog,
+ invoices,
+ services,
+ tickets,
+ users,
+ warehouse_actions,
+ warehouses,
+);
diff --git a/src/web/mod.rs b/src/web/mod.rs
index 4f03aa9..ba6e387 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -7,8 +7,8 @@ pub mod pages;
-pub async fn start() -> anyhow::Result<()> {
- let addr = "0.0.0.0:3000";
+pub async fn start(cfg: &crate::config::Config) -> anyhow::Result<()> {
+ let addr = format!("{}:{}", cfg.web.host, cfg.web.port);
let app = Router::new()
.route("/", get(pages::home::get_page))
.route("/login", get(pages::login::get_page))
@@ -18,7 +18,7 @@ pub async fn start() -> anyhow::Result<()> {
.service(ServeDir::new("static"))
);
- let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+ let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
log::info!("Listening on http://{addr}");
axum::serve(listener, app).await.unwrap();
Ok(())
diff --git a/src/web/pages/clients.rs b/src/web/pages/clients.rs
new file mode 100644
index 0000000..4c73322
--- /dev/null
+++ b/src/web/pages/clients.rs
@@ -0,0 +1,51 @@
+
+
+
+use askama::Template;
+use axum::response::{Html, IntoResponse, Response};
+
+use axum::{
+ routing::{get, post},
+ http::StatusCode,
+ Json, Router,
+};
+
+use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
+
+#[derive(Template)]
+#[template(path = "home.html")]
+pub struct PageTemplate {
+ pub ctx: BaseTemplateCtx,
+
+}
+
+impl BaseTemplate for PageTemplate {
+ fn ctx(&self) -> &BaseTemplateCtx {
+ &self.ctx
+ }
+ fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
+ &mut self.ctx
+ }
+
+}
+
+#[axum::debug_handler]
+pub async fn get_page() -> Response {
+ fn inner() -> anyhow::Result<(StatusCode, String)> {
+ let mut template = PageTemplate {
+ ctx: Default::default()
+ };
+
+ template.set_title("Home");
+
+ Ok((StatusCode::OK, template.render()?))
+ }
+
+ match inner() {
+ Ok((status, s)) => (status, Html(s)).into_response(),
+ Err(e) => {
+ let s = crate::web::pages::error::get_error_page(e.to_string()).await;
+ (StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
+ }
+ }
+}
diff --git a/src/web/pages/error.rs b/src/web/pages/error.rs
index 8b9b295..dce6785 100644
--- a/src/web/pages/error.rs
+++ b/src/web/pages/error.rs
@@ -3,12 +3,12 @@ use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "error.html")]
-pub struct ErrorTemplate {
+pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
pub error: String
}
-impl BaseTemplate for ErrorTemplate {
+impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
@@ -20,7 +20,7 @@ impl BaseTemplate for ErrorTemplate {
#[axum::debug_handler]
pub async fn get_error_page(e: String) -> String {
- let mut template = ErrorTemplate {
+ let mut template = PageTemplate {
ctx: Default::default(),
error: e.to_string()
};
diff --git a/src/web/pages/home.rs b/src/web/pages/home.rs
index 60466b7..4c73322 100644
--- a/src/web/pages/home.rs
+++ b/src/web/pages/home.rs
@@ -14,12 +14,12 @@ use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
-pub struct HomeTemplate {
+pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
-impl BaseTemplate for HomeTemplate {
+impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
@@ -32,7 +32,7 @@ impl BaseTemplate for HomeTemplate {
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
- let mut template = HomeTemplate {
+ let mut template = PageTemplate {
ctx: Default::default()
};
diff --git a/src/web/pages/inventory.rs b/src/web/pages/inventory.rs
new file mode 100644
index 0000000..60466b7
--- /dev/null
+++ b/src/web/pages/inventory.rs
@@ -0,0 +1,51 @@
+
+
+
+use askama::Template;
+use axum::response::{Html, IntoResponse, Response};
+
+use axum::{
+ routing::{get, post},
+ http::StatusCode,
+ Json, Router,
+};
+
+use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
+
+#[derive(Template)]
+#[template(path = "home.html")]
+pub struct HomeTemplate {
+ pub ctx: BaseTemplateCtx,
+
+}
+
+impl BaseTemplate for HomeTemplate {
+ fn ctx(&self) -> &BaseTemplateCtx {
+ &self.ctx
+ }
+ fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
+ &mut self.ctx
+ }
+
+}
+
+#[axum::debug_handler]
+pub async fn get_page() -> Response {
+ fn inner() -> anyhow::Result<(StatusCode, String)> {
+ let mut template = HomeTemplate {
+ ctx: Default::default()
+ };
+
+ template.set_title("Home");
+
+ Ok((StatusCode::OK, template.render()?))
+ }
+
+ match inner() {
+ Ok((status, s)) => (status, Html(s)).into_response(),
+ Err(e) => {
+ let s = crate::web::pages::error::get_error_page(e.to_string()).await;
+ (StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
+ }
+ }
+}
diff --git a/src/web/pages/login.rs b/src/web/pages/login.rs
index e824252..b1dfcbd 100644
--- a/src/web/pages/login.rs
+++ b/src/web/pages/login.rs
@@ -11,12 +11,12 @@ use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "login.html")]
-pub struct HomeTemplate {
+pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
-impl BaseTemplate for HomeTemplate {
+impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
@@ -29,7 +29,7 @@ impl BaseTemplate for HomeTemplate {
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
- let mut template = HomeTemplate {
+ let mut template = PageTemplate {
ctx: Default::default()
};
diff --git a/src/web/pages/mod.rs b/src/web/pages/mod.rs
index d4fc7a0..91fa2b4 100644
--- a/src/web/pages/mod.rs
+++ b/src/web/pages/mod.rs
@@ -1,6 +1,8 @@
-
+pub mod clients;
+pub mod inventory;
+pub mod tickets;
pub mod home;
pub mod login;
pub mod error;
diff --git a/src/web/pages/tickets.rs b/src/web/pages/tickets.rs
new file mode 100644
index 0000000..0ecb5a3
--- /dev/null
+++ b/src/web/pages/tickets.rs
@@ -0,0 +1,51 @@
+
+
+
+use askama::Template;
+use axum::response::{Html, IntoResponse, Response};
+
+use axum::{
+ routing::{get, post},
+ http::StatusCode,
+ Json, Router,
+};
+
+use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
+
+#[derive(Template)]
+#[template(path = "home.html")]
+pub struct PageTemplate {
+ pub ctx: BaseTemplateCtx,
+
+}
+
+impl BaseTemplate for PageTemplate {
+ fn ctx(&self) -> &BaseTemplateCtx {
+ &self.ctx
+ }
+ fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
+ &mut self.ctx
+ }
+
+}
+
+#[axum::debug_handler]
+pub async fn get_page() -> Response {
+ fn inner() -> anyhow::Result<(StatusCode, String)> {
+ let mut template = PageTemplate {
+ ctx: Default::default()
+ };
+
+ template.set_title("Tickets");
+
+ Ok((StatusCode::OK, template.render()?))
+ }
+
+ match inner() {
+ Ok((status, s)) => (status, Html(s)).into_response(),
+ Err(e) => {
+ let s = crate::web::pages::error::get_error_page(e.to_string()).await;
+ (StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
+ }
+ }
+}