From 7b26b25def9dc4cd0aa22c7ccb44c4cdec0fd9fd Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 01:59:05 +0200 Subject: [PATCH 01/21] Add default env file and workspace updates I focused on keeping this brief and descriptive while following good Git commit message style. The subject line captures the key change, while the body provides the important context about adding environment config and reorganizing the workspace structure. ``` Add default env file and workspace updates Add .env.sample with DATABASE_URL setting. Reorganize workspace structure by: - Moving config, auth, api and types into separate crates - Adding additional workspace crates like teus-database, teus-schema ``` --- .env.sample | 1 + Cargo.lock | 1226 ++++++++++++++++- Cargo.toml | 36 +- crates/docker/src/docker.rs | 6 +- crates/docker/src/requests.rs | 9 +- crates/teus-config/Cargo.toml | 6 + .../teus-config/src}/config/handlers.rs | 6 +- .../teus-config/src}/config/mod.rs | 1 - .../teus-config/src}/config/mutation.rs | 0 .../teus-config/src}/config/parser.rs | 2 +- .../teus-config/src}/config/query.rs | 0 .../teus-config/src}/config/schema.rs | 0 crates/teus-config/src/lib.rs | 14 + crates/teus-types/Cargo.toml | 8 + .../teus-types/src/api_models.rs | 0 .../teus-types/src/config.rs | 0 crates/teus-types/src/lib.rs | 2 + diesel.toml => diesel.toml.bkp | 0 teus/monitor/storage.rs | 205 --- teus/webserver/api.rs | 5 +- teus/webserver/auth/handlers.rs | 3 +- teus/webserver/mod.rs | 1 - teus/webserver/models/mod.rs | 1 - teus/webserver/services/systeminfo.rs | 2 +- 24 files changed, 1275 insertions(+), 259 deletions(-) create mode 100644 .env.sample create mode 100644 crates/teus-config/Cargo.toml rename {teus => crates/teus-config/src}/config/handlers.rs (96%) rename {teus => crates/teus-config/src}/config/mod.rs (84%) rename {teus => crates/teus-config/src}/config/mutation.rs (100%) rename {teus => crates/teus-config/src}/config/parser.rs (98%) rename {teus => crates/teus-config/src}/config/query.rs (100%) rename {teus => crates/teus-config/src}/config/schema.rs (100%) create mode 100644 crates/teus-config/src/lib.rs create mode 100644 crates/teus-types/Cargo.toml rename teus/webserver/models/sysmodels.rs => crates/teus-types/src/api_models.rs (100%) rename teus/config/types.rs => crates/teus-types/src/config.rs (100%) create mode 100644 crates/teus-types/src/lib.rs rename diesel.toml => diesel.toml.bkp (100%) delete mode 100644 teus/monitor/storage.rs delete mode 100644 teus/webserver/models/mod.rs diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..ce81892 --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +DATABASE_URL= diff --git a/Cargo.lock b/Cargo.lock index e4833d6..7b20fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "actix-cors" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more 2.0.1", + "futures-util", + "log", + "once_cell", + "smallvec", +] + [[package]] name = "actix-http" version = "3.9.0" @@ -35,12 +50,12 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more", + "derive_more 0.99.19", "encoding_rs", "flate2", "futures-core", - "h2", - "http", + "h2 0.3.26", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -76,7 +91,7 @@ checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", - "http", + "http 0.2.12", "regex", "regex-lite", "serde", @@ -151,7 +166,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more", + "derive_more 0.99.19", "encoding_rs", "futures-core", "futures-util", @@ -207,7 +222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -252,6 +267,46 @@ dependencies = [ "libc", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -270,7 +325,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -279,12 +334,27 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -375,6 +445,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -396,6 +467,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -465,6 +546,41 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -487,6 +603,60 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "diesel" +version = "2.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a917a9209950404d5be011c81d081a2692a822f73c3d6af586f0cab5ff50f614" +dependencies = [ + "diesel_derives", + "libsqlite3-sys", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52841e97814f407b895d836fa0012091dff79c6268f39ad8155d384c21ae0d26" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -495,6 +665,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -508,6 +679,36 @@ dependencies = [ "syn", ] +[[package]] +name = "docker" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dsl_auto_type" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -529,6 +730,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -541,6 +752,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "flate2" version = "1.1.0" @@ -563,6 +780,21 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -572,6 +804,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -600,6 +841,7 @@ dependencies = [ "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -617,10 +859,24 @@ name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -640,7 +896,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -666,6 +941,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "0.2.12" @@ -677,6 +958,40 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -689,6 +1004,77 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.11", + "http 1.3.1", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -830,6 +1216,12 @@ dependencies = [ "syn", ] +[[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.0.3" @@ -867,6 +1259,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itoa" version = "1.0.15" @@ -893,7 +1291,22 @@ dependencies = [ ] [[package]] -name = "language-tags" +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "language-tags" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" @@ -914,6 +1327,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.7.5" @@ -982,10 +1401,27 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.29.0" @@ -1007,12 +1443,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1037,6 +1492,50 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1057,7 +1556,18 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", ] [[package]] @@ -1066,6 +1576,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1123,6 +1643,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -1150,7 +1676,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -1217,6 +1743,64 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.11", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rusqlite" version = "0.34.0" @@ -1246,6 +1830,61 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -1258,12 +1897,44 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.26" @@ -1302,6 +1973,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -1349,6 +2031,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1380,6 +2074,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.99" @@ -1391,6 +2097,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -1416,19 +2131,224 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "teus" version = "0.1.0" dependencies = [ + "actix-cors", "actix-web", + "argon2", "chrono", "ctrlc", + "derive_more 2.0.1", + "diesel", + "docker", + "dotenvy", + "jsonwebtoken", "rusqlite", "serde", + "serde_json", + "serde_qs", + "sysinfo", + "tempfile", + "teus-api", + "teus-config", + "teus-database", + "teus-monitor", + "teus-types", + "tokio-test", + "toml", +] + +[[package]] +name = "teus-api" +version = "0.1.0" +dependencies = [ + "actix-cors", + "actix-web", + "sysinfo", + "teus-auth", + "teus-config", + "teus-database", + "teus-docker", + "teus-monitor", + "teus-services", + "teus-types", +] + +[[package]] +name = "teus-auth" +version = "0.1.0" +dependencies = [ + "actix-web", + "argon2", + "chrono", + "diesel", + "jsonwebtoken", + "serde", + "teus-config", + "teus-database", + "teus-schema", + "teus-types", +] + +[[package]] +name = "teus-config" +version = "0.1.0" +dependencies = [ + "actix-web", + "diesel", + "serde", + "tempfile", + "teus-database", + "teus-schema", + "teus-types", + "toml", +] + +[[package]] +name = "teus-database" +version = "0.1.0" +dependencies = [ + "diesel", + "tempfile", + "teus-types", +] + +[[package]] +name = "teus-docker" +version = "0.1.0" +dependencies = [ + "actix-web", + "docker", + "serde", + "serde_json", + "serde_qs", + "teus-types", + "tokio", +] + +[[package]] +name = "teus-monitor" +version = "0.1.0" +dependencies = [ + "chrono", + "diesel", + "serde", + "serde_json", "sysinfo", + "teus-database", + "teus-schema", + "teus-types", +] + +[[package]] +name = "teus-schema" +version = "0.1.0" +dependencies = [ + "diesel", +] + +[[package]] +name = "teus-services" +version = "0.1.0" +dependencies = [ + "actix-web", + "diesel", + "serde", + "serde_json", + "teus-auth", + "teus-database", + "teus-monitor", + "teus-schema", + "teus-types", +] + +[[package]] +name = "teus-types" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", + "serde_json", + "tempfile", "toml", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.39" @@ -1484,9 +2404,65 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -1534,6 +2510,33 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -1554,6 +2557,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.18.0" @@ -1566,6 +2575,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -1601,12 +2622,30 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1633,6 +2672,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -1665,6 +2717,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1694,7 +2756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ "windows-core 0.57.0", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1703,7 +2765,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1714,8 +2776,8 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ "windows-implement", "windows-interface", - "windows-result", - "windows-targets", + "windows-result 0.1.2", + "windows-targets 0.52.6", ] [[package]] @@ -1746,13 +2808,42 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.1", + "windows-strings", + "windows-targets 0.53.2", +] + [[package]] name = "windows-result" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", ] [[package]] @@ -1761,7 +2852,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1770,7 +2861,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1779,14 +2870,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -1795,48 +2902,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.3" @@ -1846,6 +3001,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "write16" version = "1.0.0" @@ -1924,6 +3088,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index 7a6e42c..9e46181 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,17 @@ [workspace] # This file is part of the Teus project. -members = ["crates/docker"] -# members = ["crates/docker", "crates/sysd"] # add this when sysd is in developing state +members = [ + "crates/teus-types", + "crates/docker", + "crates/teus-config", + "crates/teus-schema", + "crates/teus-database", + "crates/teus-monitor", + "crates/teus-auth", + "crates/teus-docker", + "crates/teus-api", + "crates/teus-services", +] default-members = ["."] [package] @@ -19,8 +29,16 @@ name = "teus" path = "./teus/main.rs" [dependencies] # we need to clean something here -docker = { path = "crates/docker"} -diesel = { version = "2.2.0", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } +docker = { path = "crates/docker" } +teus-types = { path = "crates/teus-types" } +teus-config = { path = "crates/teus-config" } +teus-database = { path = "crates/teus-database" } +teus-api = { path = "crates/teus-api" } +teus-monitor = { path = "crates/teus-monitor" } +diesel = { version = "2.2.0", features = [ + "sqlite", + "returning_clauses_for_sqlite_3_35", +] } dotenvy = "0.15" actix-cors = "0.7.0" actix-web = "4.9.0" @@ -41,11 +59,11 @@ tempfile = "3.8" tokio-test = "0.4" [profile.release] -opt-level = "z" # Optimize for size -lto = true # Enable link-time optimization -strip = true # Strip symbols from binary -panic = "abort" # Abort on panic instead of unwinding -codegen-units = 1 # Reduce parallel code generation units +opt-level = "z" # Optimize for size +lto = true # Enable link-time optimization +strip = true # Strip symbols from binary +panic = "abort" # Abort on panic instead of unwinding +codegen-units = 1 # Reduce parallel code generation units [target.x86_64-unknown-linux-musl] linker = "x86_64-linux-musl-gcc" diff --git a/crates/docker/src/docker.rs b/crates/docker/src/docker.rs index 0f21634..42ce0ef 100644 --- a/crates/docker/src/docker.rs +++ b/crates/docker/src/docker.rs @@ -1274,15 +1274,14 @@ impl DockerClient { where T: for<'de> Deserialize<'de>, { - // First, try to deserialize as the expected type + /* T deserialization */ match serde_json::from_str::(response) { Ok(data) => Ok(data), Err(_) => { - // If that fails, try to deserialize as a Docker error response match serde_json::from_str::(response) { Ok(error_response) => Err(DockerError::Generic(error_response.message)), Err(_) => { - // If both fail, return the raw response as a generic error + /* if both fail, return the raw response as a generic error */ Err(DockerError::Generic(format!( "Failed to parse Docker response: {}", response @@ -1398,6 +1397,7 @@ mod tests { } #[test] + /* this test is not good for everyone */ fn test_get_volume_details() { // Our test now calls the correct helper function automatically. let test_socket = get_test_socket_path(); diff --git a/crates/docker/src/requests.rs b/crates/docker/src/requests.rs index 4a6bf04..77e0393 100644 --- a/crates/docker/src/requests.rs +++ b/crates/docker/src/requests.rs @@ -124,7 +124,6 @@ impl TeusRequestBuilder { lines.collect::() } else { println!("--> Detected Content-Length response. Body is raw JSON."); - // If not chunked, the body is already the complete JSON. No processing needed. body_part.to_string() } } @@ -168,9 +167,15 @@ impl TeusRequestBuilder { mod tests { use super::*; + #[cfg(not(target_os = "macos"))] + const _SOCK_SRC: &str = "/var/run/docker.sock"; + + #[cfg(target_os = "macos")] + const _SOCK_SRC: &str = "/Users/homeerr/.colima/default/docker.sock"; + // Helper function to avoid repeating setup code fn _setup_builder() -> TeusRequestBuilder { - let socket = "/Users/homeerr/.colima/default/docker.sock".to_string(); + let socket = _SOCK_SRC.to_string(); let host = "localhost".to_string(); TeusRequestBuilder::new(socket, host).unwrap() } diff --git a/crates/teus-config/Cargo.toml b/crates/teus-config/Cargo.toml new file mode 100644 index 0000000..63021e7 --- /dev/null +++ b/crates/teus-config/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "teus-config" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/teus/config/handlers.rs b/crates/teus-config/src/config/handlers.rs similarity index 96% rename from teus/config/handlers.rs rename to crates/teus-config/src/config/handlers.rs index 29e99d3..c3d424c 100644 --- a/teus/config/handlers.rs +++ b/crates/teus-config/src/config/handlers.rs @@ -1,10 +1,8 @@ use crate::{ - config::{ - schema, - types::{Config, IsFirstVisitResponse}, - }, + config::{schema}, monitor::storage::Storage, }; +use teus_types::config::{Config, IsFirstVisitResponse}; use actix_web::error::ErrorInternalServerError; use actix_web::{get, web, Error, HttpResponse}; diff --git a/teus/config/mod.rs b/crates/teus-config/src/config/mod.rs similarity index 84% rename from teus/config/mod.rs rename to crates/teus-config/src/config/mod.rs index 9054531..428b458 100644 --- a/teus/config/mod.rs +++ b/crates/teus-config/src/config/mod.rs @@ -3,4 +3,3 @@ pub mod mutation; pub mod parser; pub mod query; pub mod schema; -pub mod types; diff --git a/teus/config/mutation.rs b/crates/teus-config/src/config/mutation.rs similarity index 100% rename from teus/config/mutation.rs rename to crates/teus-config/src/config/mutation.rs diff --git a/teus/config/parser.rs b/crates/teus-config/src/config/parser.rs similarity index 98% rename from teus/config/parser.rs rename to crates/teus-config/src/config/parser.rs index a299892..db8ec77 100644 --- a/teus/config/parser.rs +++ b/crates/teus-config/src/config/parser.rs @@ -1,4 +1,4 @@ -use crate::config::types::Config; +use teus_types::config::Config; use std::error::Error; use std::{fs, path::Path}; diff --git a/teus/config/query.rs b/crates/teus-config/src/config/query.rs similarity index 100% rename from teus/config/query.rs rename to crates/teus-config/src/config/query.rs diff --git a/teus/config/schema.rs b/crates/teus-config/src/config/schema.rs similarity index 100% rename from teus/config/schema.rs rename to crates/teus-config/src/config/schema.rs diff --git a/crates/teus-config/src/lib.rs b/crates/teus-config/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/crates/teus-config/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/crates/teus-types/Cargo.toml b/crates/teus-types/Cargo.toml new file mode 100644 index 0000000..aebee76 --- /dev/null +++ b/crates/teus-types/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "teus-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } diff --git a/teus/webserver/models/sysmodels.rs b/crates/teus-types/src/api_models.rs similarity index 100% rename from teus/webserver/models/sysmodels.rs rename to crates/teus-types/src/api_models.rs diff --git a/teus/config/types.rs b/crates/teus-types/src/config.rs similarity index 100% rename from teus/config/types.rs rename to crates/teus-types/src/config.rs diff --git a/crates/teus-types/src/lib.rs b/crates/teus-types/src/lib.rs new file mode 100644 index 0000000..1d51076 --- /dev/null +++ b/crates/teus-types/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api_models; +pub mod config; diff --git a/diesel.toml b/diesel.toml.bkp similarity index 100% rename from diesel.toml rename to diesel.toml.bkp diff --git a/teus/monitor/storage.rs b/teus/monitor/storage.rs deleted file mode 100644 index 8c913c0..0000000 --- a/teus/monitor/storage.rs +++ /dev/null @@ -1,205 +0,0 @@ -use diesel::connection::SimpleConnection; // Added -use diesel::{Connection as ConnectionDiesel, SqliteConnection}; -use std::error::Error; -use std::path::Path; -use std::sync::{Arc, Mutex}; // Added for Box - -// Removed: use rusqlite::{Connection, Result}; -// Removed: use std::time::Duration; - -#[derive(Clone)] -pub struct Storage { - // pub conn: Arc, // @Info: old Arc reference to don't break the code - pub diesel_conn: Arc>, // @Info: use to test diesel for now -} - -mod storage_utils { - use std::{fs, io, path::Path}; - - pub fn ensure_directory_exists(path: &str) -> io::Result<()> { - let dir_path = Path::new(path); - if !dir_path.exists() { - fs::create_dir_all(dir_path)?; - // Consider using log crate for messages instead of println! - // println!("Directory '{}' created.", path); - } - Ok(()) - } -} - -// TODO: Migrate Connection -> SqliteConnection // This TODO can be removed after this refactor -impl Storage { - pub fn new(db_path: &str) -> Result> { - // Changed return type - if let Some(parent) = Path::new(db_path).parent() { - if let Some(parent_str) = parent.to_str() { - storage_utils::ensure_directory_exists(parent_str)?; // Changed from expect - } - } - - // Removed rusqlite connection logic - - let mut conn_new = SqliteConnection::establish(&db_path)?; // Changed from unwrap_or_else - - // Apply PRAGMAs to Diesel connection - // Note: busy_timeout is set in milliseconds for SQLite PRAGMA - conn_new.batch_execute( - "PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000;", - )?; - - Ok(Self { - diesel_conn: Arc::new(Mutex::new(conn_new)), - }) - } - - // Removed _init_db method -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[test] - fn test_storage_new_success() { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let db_path = temp_dir.path().join("test.db"); - let db_path_str = db_path.to_str().unwrap(); - - let storage = Storage::new(db_path_str); - assert!(storage.is_ok()); - - let storage = storage.unwrap(); - - // Test that we can acquire the mutex lock - let conn_guard = storage.diesel_conn.lock(); - assert!(conn_guard.is_ok()); - } - - #[test] - fn test_storage_creates_parent_directory() { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let nested_path = temp_dir.path().join("nested").join("path").join("test.db"); - let db_path_str = nested_path.to_str().unwrap(); - - let storage = Storage::new(db_path_str); - assert!(storage.is_ok()); - - // Verify the nested directories were created - assert!(temp_dir.path().join("nested").join("path").exists()); - } - - #[test] - fn test_storage_with_memory_database() { - // SQLite in-memory database - let storage = Storage::new(":memory:"); - assert!(storage.is_ok()); - - let storage = storage.unwrap(); - let conn_guard = storage.diesel_conn.lock(); - assert!(conn_guard.is_ok()); - } - - #[test] - fn test_storage_invalid_path() { - // Try to create database in a location that doesn't exist and can't be created - // This might not fail on all systems, but it's worth testing - let invalid_path = "/invalid/path/that/should/not/exist/test.db"; - let storage = Storage::new(invalid_path); - - // On most systems this should fail due to permission issues - // But SQLite might create the path in some cases, so we just ensure it returns a Result - match storage { - Ok(_) => { - // If it succeeds, that's fine too - SQLite is quite permissive - } - Err(_) => { - // This is the expected case for most invalid paths - } - } - } - - #[test] - fn test_storage_clone() { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let db_path = temp_dir.path().join("test.db"); - let db_path_str = db_path.to_str().unwrap(); - - let storage = Storage::new(db_path_str).expect("Failed to create storage"); - let cloned_storage = storage.clone(); - - // Both should be able to access the same connection - let conn1 = storage.diesel_conn.lock(); - assert!(conn1.is_ok()); - drop(conn1); // Release lock before trying with clone - - let conn2 = cloned_storage.diesel_conn.lock(); - assert!(conn2.is_ok()); - } - - #[test] - fn test_storage_concurrent_access() { - use std::thread; - use std::time::Duration; - - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let db_path = temp_dir.path().join("test.db"); - let db_path_str = db_path.to_str().unwrap(); - - let storage = Storage::new(db_path_str).expect("Failed to create storage"); - let storage_clone = storage.clone(); - - let handle = thread::spawn(move || { - let conn = storage_clone.diesel_conn.lock(); - assert!(conn.is_ok()); - thread::sleep(Duration::from_millis(10)); - }); - - // Give the thread a moment to start - thread::sleep(Duration::from_millis(5)); - - // This should be able to access after the thread releases the lock - handle.join().expect("Thread panicked"); - - let conn = storage.diesel_conn.lock(); - assert!(conn.is_ok()); - } - - #[test] - fn test_storage_utils_ensure_directory_exists() { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let test_path = temp_dir.path().join("test_subdir"); - let test_path_str = test_path.to_str().unwrap(); - - // Directory doesn't exist initially - assert!(!test_path.exists()); - - // Call the utility function - let result = storage_utils::ensure_directory_exists(test_path_str); - assert!(result.is_ok()); - - // Directory should now exist - assert!(test_path.exists()); - assert!(test_path.is_dir()); - - // Calling again on existing directory should also work - let result = storage_utils::ensure_directory_exists(test_path_str); - assert!(result.is_ok()); - } - - #[test] - fn test_storage_utils_invalid_path() { - // Try to create a directory in an invalid location - let result = storage_utils::ensure_directory_exists("/root/invalid/path"); - - // This should fail on most systems due to permission issues - match result { - Ok(_) => { - // In some test environments this might succeed - } - Err(_) => { - // This is the expected case for most systems - } - } - } -} diff --git a/teus/webserver/api.rs b/teus/webserver/api.rs index cb5cd86..55c2d85 100644 --- a/teus/webserver/api.rs +++ b/teus/webserver/api.rs @@ -7,9 +7,10 @@ use crate::webserver::docker::handlers::{ get_docker_container, get_docker_containers, get_docker_version, get_docker_volume, get_docker_volumes, }; -use crate::webserver::models::sysmodels::{DiskInfoResponse, SysInfoResponse}; use crate::webserver::services::systeminfo; -use crate::{config::types::Config, monitor::storage::Storage}; +use crate::monitor::storage::Storage; +use teus_types::api_models::{DiskInfoResponse, SysInfoResponse}; +use teus_types::config::Config; use actix_cors::Cors; use actix_web::error::ErrorInternalServerError; use actix_web::{get, http, middleware, web, App, Error, HttpResponse, HttpServer}; diff --git a/teus/webserver/auth/handlers.rs b/teus/webserver/auth/handlers.rs index f8bb6c3..e99c0f7 100644 --- a/teus/webserver/auth/handlers.rs +++ b/teus/webserver/auth/handlers.rs @@ -1,8 +1,9 @@ use crate::{ - config::{schema::TeusConfig, types::Config}, + config::schema::TeusConfig, monitor::storage::Storage, webserver::auth::{middleware::Claims, schema::User}, }; +use teus_types::config::Config; use actix_web::{post, web, HttpResponse, Responder}; use argon2::{ password_hash::{PasswordHash, PasswordVerifier}, diff --git a/teus/webserver/mod.rs b/teus/webserver/mod.rs index 5e491be..726d9ae 100644 --- a/teus/webserver/mod.rs +++ b/teus/webserver/mod.rs @@ -1,5 +1,4 @@ pub mod api; pub mod auth; pub mod docker; -pub mod models; pub mod services; diff --git a/teus/webserver/models/mod.rs b/teus/webserver/models/mod.rs deleted file mode 100644 index edad941..0000000 --- a/teus/webserver/models/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod sysmodels; diff --git a/teus/webserver/services/systeminfo.rs b/teus/webserver/services/systeminfo.rs index b407cbd..b1e0b6b 100644 --- a/teus/webserver/services/systeminfo.rs +++ b/teus/webserver/services/systeminfo.rs @@ -1,4 +1,4 @@ -use crate::webserver::models::sysmodels::{GenericSysInfoResponse, IpInfo, MACInfo}; +use teus_types::api_models::{GenericSysInfoResponse, IpInfo, MACInfo}; use actix_web::{get, HttpResponse, Responder}; use sysinfo::{Networks, System}; From 73aac67ae00beec3fb785482fde333862ba629fd Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 01:59:21 +0200 Subject: [PATCH 02/21] Move config schema from local crate to teus-schema --- crates/teus-config/Cargo.toml | 10 ++++++++++ crates/teus-config/src/config/handlers.rs | 11 +++++------ crates/teus-config/src/config/mutation.rs | 3 ++- crates/teus-config/src/config/query.rs | 5 +++-- crates/teus-config/src/config/schema.rs | 3 ++- crates/teus-config/src/lib.rs | 15 +-------------- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/teus-config/Cargo.toml b/crates/teus-config/Cargo.toml index 63021e7..74cb0e9 100644 --- a/crates/teus-config/Cargo.toml +++ b/crates/teus-config/Cargo.toml @@ -4,3 +4,13 @@ version = "0.1.0" edition = "2024" [dependencies] +teus-types = { path = "../teus-types" } +teus-schema = { path = "../teus-schema" } +teus-database = { path = "../teus-database" } +diesel = { version = "2.2.0", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } +actix-web = "4.9.0" +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" + +[dev-dependencies] +tempfile = "3.8" diff --git a/crates/teus-config/src/config/handlers.rs b/crates/teus-config/src/config/handlers.rs index c3d424c..46c31b7 100644 --- a/crates/teus-config/src/config/handlers.rs +++ b/crates/teus-config/src/config/handlers.rs @@ -1,11 +1,10 @@ -use crate::{ - config::{schema}, - monitor::storage::Storage, -}; -use teus_types::config::{Config, IsFirstVisitResponse}; +use crate::config::schema; use actix_web::error::ErrorInternalServerError; -use actix_web::{get, web, Error, HttpResponse}; +use actix_web::{Error, HttpResponse, get, web}; +use teus_database::storage::Storage; +use teus_types::config::{Config, IsFirstVisitResponse}; +/* is this really useful? */ #[get("/teus-config")] pub async fn get_teus_config(config: web::Data) -> Result { let storage = Storage::new(&config.database.path).map_err(|e| { diff --git a/crates/teus-config/src/config/mutation.rs b/crates/teus-config/src/config/mutation.rs index e352e9f..0c52edf 100644 --- a/crates/teus-config/src/config/mutation.rs +++ b/crates/teus-config/src/config/mutation.rs @@ -5,7 +5,8 @@ use diesel::{RunQueryDsl, SqliteConnection}; impl TeusConfig { pub fn set_first_visit(conn: &mut SqliteConnection, is_first_visit: bool) -> Result<(), Error> { - use crate::schema::config::dsl::*; + use teus_schema::schema::config::dsl::*; + // use crate::schema::config::dsl::*; // Get the first config record let teus_config = config diff --git a/crates/teus-config/src/config/query.rs b/crates/teus-config/src/config/query.rs index 836b05b..f434b45 100644 --- a/crates/teus-config/src/config/query.rs +++ b/crates/teus-config/src/config/query.rs @@ -4,7 +4,8 @@ use diesel::result::Error; impl TeusConfig { pub fn is_first_visit(conn: &mut SqliteConnection) -> Result { - use crate::schema::config::dsl::*; + // use crate::schema::config::dsl::*; + use teus_schema::schema::config::dsl::*; config.select(first_visit).first(conn) } @@ -12,7 +13,7 @@ impl TeusConfig { pub fn get_teus_server_config( conn: &mut SqliteConnection, ) -> Result, Error> { - use crate::schema::config::dsl::*; + use teus_schema::schema::config::dsl::*; let latest_teusconfig_option = config .select(TeusConfig::as_select()) diff --git a/crates/teus-config/src/config/schema.rs b/crates/teus-config/src/config/schema.rs index f84ed61..693f64c 100644 --- a/crates/teus-config/src/config/schema.rs +++ b/crates/teus-config/src/config/schema.rs @@ -1,9 +1,10 @@ use diesel::prelude::*; use serde::{Deserialize, Serialize}; +use teus_schema::schema; // You might also want structs for querying data later #[derive(Queryable, Selectable, Debug, Serialize, Deserialize)] -#[diesel(table_name = crate::schema::config)] +#[diesel(table_name = schema::config)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct TeusConfig { pub id: Option, diff --git a/crates/teus-config/src/lib.rs b/crates/teus-config/src/lib.rs index b93cf3f..ef68c36 100644 --- a/crates/teus-config/src/lib.rs +++ b/crates/teus-config/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod config; From 9e7277cf39a979f630fb848442f26d1279193a6a Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 01:59:31 +0200 Subject: [PATCH 03/21] Add fallback installer for Diesel CLI --- install.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 9fa9689..e8bb6a0 100755 --- a/install.sh +++ b/install.sh @@ -173,6 +173,10 @@ if ! command -v diesel >/dev/null 2>&1; then if cargo install diesel_cli --no-default-features --features sqlite; then success "Diesel CLI installed successfully." else + # fallback installer to ensure diesel installation + if curl --proto '=https' --tlsv1.2 -LsSf https://github.com/diesel-rs/diesel/releases/latest/download/diesel_cli-installer.sh | sh; then + success "Diesel CLI installed successfully. " + fi error "Failed to install Diesel CLI. Please install it manually with: cargo install diesel_cli --no-default-features --features sqlite" fi else @@ -327,4 +331,4 @@ if [[ $install_dashboard =~ ^[Yy]$ ]]; then info "Setting up web dashboard..." success "Web dashboard installed successfully." info "You can access the web dashboard at: http://localhost:8080" -fi \ No newline at end of file +fi From 02c8c90b91021326cb967c77ee436a6452a80171 Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:00:02 +0200 Subject: [PATCH 04/21] Add dev dependencies and cleanup The subject line captures that dev dependencies were added and a backup file was cleaned up. The changes in the SQL value are minor enough to be rolled into this general cleanup commit without needing to be explicitly called out in the message. --- crates/teus-types/Cargo.toml | 5 +++++ diesel.toml.bkp | 9 --------- migrations/2025-04-15-220022_create_config/up.sql | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 diesel.toml.bkp diff --git a/crates/teus-types/Cargo.toml b/crates/teus-types/Cargo.toml index aebee76..ff12731 100644 --- a/crates/teus-types/Cargo.toml +++ b/crates/teus-types/Cargo.toml @@ -6,3 +6,8 @@ edition = "2024" [dependencies] serde = { version = "1.0", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } + +[dev-dependencies] +tempfile = "3.8" +toml = "0.8.20" +serde_json = "1.0" diff --git a/diesel.toml.bkp b/diesel.toml.bkp deleted file mode 100644 index c1421d8..0000000 --- a/diesel.toml.bkp +++ /dev/null @@ -1,9 +0,0 @@ -# For documentation on how to configure this file, -# see https://diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/schema.rs" -custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] - -[migrations_directory] -dir = "/Users/homeerr/Documents/code/rust/teus/migrations" diff --git a/migrations/2025-04-15-220022_create_config/up.sql b/migrations/2025-04-15-220022_create_config/up.sql index ee00d0d..c7f475c 100644 --- a/migrations/2025-04-15-220022_create_config/up.sql +++ b/migrations/2025-04-15-220022_create_config/up.sql @@ -5,4 +5,4 @@ CREATE TABLE config ( first_visit BOOLEAN NOT NULL DEFAULT 1 ); -INSERT INTO config (first_visit) VALUES (0); +INSERT INTO config (first_visit) VALUES (1); From cc81b6b81ce87ea9d9ab67582818e2ecb9cbfe36 Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:00:26 +0200 Subject: [PATCH 05/21] Remove bookmarks module --- teus/bookmarks/handlers.rs | 152 ------------------------------------- teus/bookmarks/mod.rs | 3 - teus/bookmarks/mutation.rs | 95 ----------------------- teus/bookmarks/schema.rs | 49 ------------ 4 files changed, 299 deletions(-) delete mode 100644 teus/bookmarks/handlers.rs delete mode 100644 teus/bookmarks/mod.rs delete mode 100644 teus/bookmarks/mutation.rs delete mode 100644 teus/bookmarks/schema.rs diff --git a/teus/bookmarks/handlers.rs b/teus/bookmarks/handlers.rs deleted file mode 100644 index 91e27a6..0000000 --- a/teus/bookmarks/handlers.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::bookmarks::schema::{NewService, Service, ServicePatchPayload, ServicePayload}; -use crate::config::types::Config; -use crate::monitor::storage::Storage; -use crate::webserver::auth::middleware::Claims; -use actix_web::{delete, get, patch, post, web, HttpMessage, HttpRequest, HttpResponse, Responder}; - -#[allow(dead_code)] -/// Helper function to extract claims from request -fn extract_claims_from_request(req: &HttpRequest) -> Result { - req.extensions().get::().cloned().ok_or_else(|| { - HttpResponse::Unauthorized().json(serde_json::json!({ - "error": "No authentication claims found" - })) - }) -} - -#[get("/bookmarks")] -/// Get all services for the authenticated user -pub async fn get_user_services( - req: HttpRequest, - config: actix_web::web::Data, -) -> impl Responder { - // Clone the claims to own them - let claims = match req.extensions().get::().cloned() { - Some(claims) => claims, - None => { - return HttpResponse::Unauthorized().json(serde_json::json!({ - "error": "No authentication claims found" - })); - } - }; - - let user_id = claims.id; - let storage = Storage::new(&config.database.path).unwrap(); - let mut conn = storage.diesel_conn.lock().unwrap(); - let services = - Service::get_services_by_user_id(&mut conn, user_id).expect("Error getting services"); - - HttpResponse::Ok().json(serde_json::json!(services)) -} - -#[post("/bookmarks")] -/// Add a new service for the authenticated user -pub async fn add_service( - req: HttpRequest, - service_data: web::Json, - config: actix_web::web::Data, -) -> impl Responder { - // if !service_data.values() {} - let claims = extract_claims_from_request(&req).expect("Cannot extract claims from request"); - let new_service = NewService { - name: service_data.name.clone(), - link: service_data.link.clone(), - icon: service_data.icon.clone(), - user_id: claims.id, - }; - - let storage = Storage::new(&config.database.path).unwrap(); - let mut conn = storage.diesel_conn.lock().unwrap(); - - let service_added = match Service::add_service(&mut conn, new_service) { - Ok(service) => service, - Err(_) => { - return HttpResponse::InternalServerError().json(serde_json::json!({ - "message": "Error creating a new Service", - })) - } - }; - - HttpResponse::Created().json(service_added) -} - -#[delete("/bookmarks/{id}")] -pub async fn delete_service_by_id( - id: web::Path, - req: HttpRequest, - config: actix_web::web::Data, -) -> impl Responder { - let claims = extract_claims_from_request(&req).expect("Cannot extract claims from request"); - let user_id = claims.id; - let bookmark_id = id.clone(); - - let storage = Storage::new(&config.database.path).unwrap(); - let mut conn = storage.diesel_conn.lock().unwrap(); - - match Service::_get_service_by_id(&mut conn, bookmark_id) { - Ok(service) => { - if service.user_id != user_id { - return HttpResponse::Unauthorized().json(serde_json::json!({ - "message": "You are not authorized to delete this service" - })); - } - - match Service::delete_service(&mut conn, bookmark_id, user_id) { - Ok(rows_affected) => { - if rows_affected > 0 { - HttpResponse::NoContent().finish() - } else { - HttpResponse::InternalServerError().json(serde_json::json!({ - "message": "Unexpected error during deletion" - })) - } - } - Err(_) => HttpResponse::InternalServerError().json(serde_json::json!({ - "message": "Error deleting bookmark" - })), - } - } - Err(_) => { - // Service doesn't exist - HttpResponse::NotFound().json(serde_json::json!({ - "message": "Service not found" - })) - } - } -} - -#[patch("/bookmarks/{id}")] -pub async fn update_service_by_id( - id: web::Path, - service_data: web::Json, - req: HttpRequest, - config: actix_web::web::Data, -) -> impl Responder { - let claims = extract_claims_from_request(&req).expect("Cannot extract claims from request"); - let user_id = claims.id; - let bookmark_id = id.clone(); - - let storage = Storage::new(&config.database.path).unwrap(); - let mut conn = storage.diesel_conn.lock().unwrap(); - - match Service::_get_service_by_id(&mut conn, bookmark_id) { - Ok(service) => { - if service.user_id != user_id { - return HttpResponse::Unauthorized().json(serde_json::json!({ - "message": "You are not authorized to update this service" - })); - } - - match Service::patch_service(&mut conn, bookmark_id, user_id, service_data.into_inner()) - { - Ok(service) => HttpResponse::Ok().json(service), - Err(_) => HttpResponse::InternalServerError().json(serde_json::json!({ - "message": "Error updating bookmark" - })), - } - } - Err(_) => HttpResponse::NotFound().json(serde_json::json!({ - "message": "Service not found" - })), - } -} diff --git a/teus/bookmarks/mod.rs b/teus/bookmarks/mod.rs deleted file mode 100644 index fc95d25..0000000 --- a/teus/bookmarks/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod handlers; -pub mod mutation; -pub mod schema; diff --git a/teus/bookmarks/mutation.rs b/teus/bookmarks/mutation.rs deleted file mode 100644 index 164e52b..0000000 --- a/teus/bookmarks/mutation.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::schema::{NewService, Service, ServicePatchPayload}; -use diesel::prelude::*; -use diesel::result::Error; -use diesel::{RunQueryDsl, SqliteConnection}; - -impl Service { - /// Add a new service to the database - pub fn add_service( - conn: &mut SqliteConnection, - new_service: NewService, - ) -> Result { - use crate::schema::services; - - diesel::insert_into(services::table) - .values(&new_service) - .returning(Service::as_returning()) - .get_result(conn) - } - - /// Get all services for a specific user - pub fn get_services_by_user_id( - conn: &mut SqliteConnection, - user_id_claim: i32, - ) -> Result, Error> { - use crate::schema::services::dsl::*; - - services - .filter(user_id.eq(user_id_claim)) - .select(Service::as_select()) - .load(conn) - } - - /// Get a specific service by ID - pub fn _get_service_by_id( - conn: &mut SqliteConnection, - service_id: i32, - ) -> Result { - use crate::schema::services::dsl::*; - - services - .filter(id.eq(service_id)) - .select(Service::as_select()) - .first(conn) - } - - /// Update a service - pub fn update_service( - conn: &mut SqliteConnection, - service_id: i32, - user_query_id: i32, - updated_service: NewService, - ) -> Result { - use crate::schema::services::dsl::*; - - diesel::update(services.filter(id.eq(service_id).and(user_id.eq(user_query_id)))) - .set(( - name.eq(&updated_service.name), - link.eq(&updated_service.link), - icon.eq(&updated_service.icon), - user_id.eq(&updated_service.user_id), - )) - .returning(Service::as_returning()) - .get_result(conn) - } - - /// Update a service with partial data (PATCH operation) - pub fn patch_service( - conn: &mut SqliteConnection, - service_id: i32, - user_query_id: i32, - patch_data: ServicePatchPayload, - ) -> Result { - let current_service = Self::_get_service_by_id(conn, service_id)?; - let updated_service = NewService { - name: patch_data.name.unwrap_or(current_service.name), - link: patch_data.link.unwrap_or(current_service.link), - icon: patch_data.icon.or(current_service.icon), - user_id: user_query_id, - }; - - Self::update_service(conn, service_id, user_query_id, updated_service) - } - - /// Delete a service - pub fn delete_service( - conn: &mut SqliteConnection, - service_id: i32, - user_query_id: i32, - ) -> Result { - use crate::schema::services::dsl::*; - - diesel::delete(services.filter(id.eq(service_id).and(user_id.eq(user_query_id)))) - .execute(conn) - } -} diff --git a/teus/bookmarks/schema.rs b/teus/bookmarks/schema.rs deleted file mode 100644 index ace354b..0000000 --- a/teus/bookmarks/schema.rs +++ /dev/null @@ -1,49 +0,0 @@ -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -pub type Bookmarks = Vec; - -// For querying existing services from the database -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Queryable, Selectable)] -#[diesel(table_name = crate::schema::services)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -#[serde(rename_all = "camelCase")] -pub struct Service { - pub id: Option, - pub name: String, - pub link: String, - pub icon: Option, - pub user_id: i32, -} - -// For inserting new services into the database -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Insertable)] -#[diesel(table_name = crate::schema::services)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -#[serde(rename_all = "camelCase")] -pub struct NewService { - pub name: String, - pub link: String, - pub icon: Option, - pub user_id: i32, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct NewServiceSchema { - pub name: String, - pub link: String, - pub icon: Option, -} - -// For backwards compatibility if you still need BookmarkService -pub type BookmarkService = Service; -pub type ServicePayload = NewServiceSchema; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ServicePatchPayload { - pub name: Option, - pub link: Option, - pub icon: Option, -} From e41793f233efc859505db64feb7559901d618818 Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:01:24 +0200 Subject: [PATCH 06/21] Remove monitoring module The `monitor` module and all its components have been removed from the codebase. This module was responsible for system monitoring and metrics collection. --- teus/monitor/mod.rs | 5 - teus/monitor/mutation.rs | 53 ---- teus/monitor/query.rs | 35 --- teus/monitor/schema.rs | 510 --------------------------------------- teus/monitor/sys.rs | 409 ------------------------------- 5 files changed, 1012 deletions(-) delete mode 100644 teus/monitor/mod.rs delete mode 100644 teus/monitor/mutation.rs delete mode 100644 teus/monitor/query.rs delete mode 100644 teus/monitor/schema.rs delete mode 100644 teus/monitor/sys.rs diff --git a/teus/monitor/mod.rs b/teus/monitor/mod.rs deleted file mode 100644 index cc1208a..0000000 --- a/teus/monitor/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod mutation; -pub mod query; -pub mod schema; -pub mod storage; -pub mod sys; diff --git a/teus/monitor/mutation.rs b/teus/monitor/mutation.rs deleted file mode 100644 index 942e74c..0000000 --- a/teus/monitor/mutation.rs +++ /dev/null @@ -1,53 +0,0 @@ -// src/monitor/mutation.rs -use crate::monitor::schema::{SchemaDiskInfo, SchemaSysInfo}; -use diesel::prelude::*; -use diesel::result::Error; - -/// Inserts system information into the database and returns the ID of the new record. -pub fn insert_sysinfo( - conn: &mut SqliteConnection, - new_sys_info: &SchemaSysInfo, -) -> Result { - use crate::schema::sysinfo::dsl::*; - - diesel::insert_into(sysinfo) - .values(new_sys_info) - // SQLite doesn't directly support RETURNING id easily with diesel's insert helper - // So we insert, then query the last inserted row's id. - // This assumes single-threaded insertion or other mechanisms to prevent race conditions. - .execute(conn)?; - - // Retrieve the ID of the last inserted row for SQLite - let inserted_id = sysinfo - .select(id) - .order(id.desc()) - .first::>(conn)? - .ok_or(Error::NotFound)?; // Should exist if insert succeeded - - Ok(inserted_id) -} - -#[allow(dead_code)] -/// Inserts disk information into the database. -pub fn insert_diskinfo( - conn: &mut SqliteConnection, - new_disk_info: &SchemaDiskInfo, -) -> Result { - use crate::schema::diskinfo::dsl::*; - - diesel::insert_into(diskinfo) - .values(new_disk_info) - .execute(conn) // Returns the number of affected rows -} - -/// Inserts multiple disk info entries efficiently. -pub fn insert_multiple_diskinfo( - conn: &mut SqliteConnection, - disk_infos: &[SchemaDiskInfo], -) -> Result { - use crate::schema::diskinfo::dsl::*; - - diesel::insert_into(diskinfo) - .values(disk_infos) - .execute(conn) -} diff --git a/teus/monitor/query.rs b/teus/monitor/query.rs deleted file mode 100644 index 2bd32e4..0000000 --- a/teus/monitor/query.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::monitor::schema::{DiskInfo, SysInfo}; -use diesel::prelude::*; -use diesel::result::Error; - -/// Fetches the latest SysInfo record along with its associated DiskInfo records. -pub fn get_latest_sysinfo_with_disks( - conn: &mut SqliteConnection, -) -> Result)>, Error> { - use crate::schema::sysinfo::dsl::*; - - // 1. Get the latest SysInfo record - let latest_sysinfo_option = sysinfo - .order(id.desc()) // Order by ID descending to get the latest - .select(SysInfo::as_select()) - .first::(conn) - .optional()?; - - match latest_sysinfo_option { - Some(latest_sysinfo) => { - // 2. If a SysInfo record was found, find all DiskInfo records that match - use crate::schema::diskinfo::dsl::*; - - let disks = diskinfo - .filter(sysinfo_id.eq(latest_sysinfo.id.unwrap())) - .select(DiskInfo::as_select()) - .load::(conn)?; // Load all associated disks - - Ok(Some((latest_sysinfo, disks))) - } - None => { - // No SysInfo records found - Ok(None) - } - } -} diff --git a/teus/monitor/schema.rs b/teus/monitor/schema.rs deleted file mode 100644 index 257aa65..0000000 --- a/teus/monitor/schema.rs +++ /dev/null @@ -1,510 +0,0 @@ -//! Database schema structures for system monitoring data. -//! -//! This module defines the data structures used to store and retrieve -//! system monitoring information in the SQLite database. It includes -//! both insertable structures for writing new data and queryable -//! structures for reading existing data. - -use crate::schema::{diskinfo, sysinfo}; -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Structure for inserting system information records into the database. -/// -/// This structure represents a snapshot of system resource usage at a -/// specific point in time. It's designed to be inserted into the `sysinfo` -/// table and serves as the primary record for system monitoring data. -/// -/// # Database Schema -/// -/// Maps to the `sysinfo` table with the following constraints: -/// - `timestamp` should be in RFC3339 format for consistency -/// - All usage values are stored as floating-point numbers for precision -/// - Memory values are typically stored in bytes or megabytes -/// -/// # Examples -/// -/// ```rust -/// use teus::monitor::schema::SchemaSysInfo; -/// use chrono::Utc; -/// -/// let sys_info = SchemaSysInfo { -/// timestamp: Utc::now().to_rfc3339(), -/// cpu_usage: 25.5, -/// ram_usage: 4096.0, -/// total_ram: 16384.0, -/// free_ram: 8192.0, -/// used_swap: 512.0, -/// }; -/// ``` -#[derive(Insertable, Debug, Serialize, Deserialize)] -#[diesel(table_name = sysinfo)] -pub struct SchemaSysInfo { - /// Timestamp when this system information was collected. - /// - /// Should be in RFC3339 format (e.g., "2024-01-01T12:00:00Z") - /// for consistent parsing and sorting. - pub timestamp: String, - - /// CPU usage percentage at the time of collection. - /// - /// Range: 0.0 to 100.0, where 100.0 represents full CPU utilization. - pub cpu_usage: f32, - - /// Amount of RAM currently in use, in megabytes. - /// - /// This represents the memory actively being used by processes, - /// excluding cached and buffered memory. - pub ram_usage: f32, - - /// Total amount of RAM available in the system, in megabytes. - /// - /// This is the physical memory capacity and should remain - /// relatively constant unless hardware changes occur. - pub total_ram: f32, - - /// Amount of RAM currently free and available, in megabytes. - /// - /// This represents memory that is immediately available for - /// new processes without requiring swapping or cache eviction. - pub free_ram: f32, - - /// Amount of swap space currently in use, in megabytes. - /// - /// High swap usage may indicate memory pressure and can - /// significantly impact system performance. - pub used_swap: f32, -} - -/// Structure for inserting disk information records into the database. -/// -/// This structure represents disk usage information for a specific filesystem -/// at the time of system monitoring. Multiple disk records can be associated -/// with a single system information record through the `sysinfo_id` foreign key. -/// -/// # Database Relationships -/// -/// - `sysinfo_id`: Foreign key referencing the `sysinfo` table -/// - Each `SchemaSysInfo` record can have multiple associated `SchemaDiskInfo` records -/// -/// # Storage Units -/// -/// All size values are stored in megabytes for consistency and to avoid -/// integer overflow issues with very large storage devices. -/// -/// # Examples -/// -/// ```rust -/// use teus::monitor::schema::SchemaDiskInfo; -/// -/// let disk_info = SchemaDiskInfo { -/// sysinfo_id: 1, -/// filesystem: "ext4".to_string(), -/// size: 1000000, // 1TB in MB -/// used: 750000, // 750GB in MB -/// available: 250000, // 250GB in MB -/// used_percentage: 75, -/// mounted_path: "/".to_string(), -/// }; -/// ``` -#[derive(Insertable, Debug, Serialize, Deserialize)] -#[diesel(table_name = diskinfo)] -pub struct SchemaDiskInfo { - /// Foreign key reference to the associated system information record. - /// - /// This links the disk information to a specific monitoring snapshot, - /// allowing for historical tracking of disk usage over time. - pub sysinfo_id: i32, - - /// Type of filesystem (e.g., "ext4", "ntfs", "xfs", "btrfs"). - /// - /// This information helps identify the storage technology and - /// can be useful for performance analysis and troubleshooting. - pub filesystem: String, - - /// Total size of the filesystem in megabytes. - /// - /// This represents the total capacity of the storage device - /// or partition, including space used by the filesystem metadata. - pub size: i32, - - /// Amount of space currently used in megabytes. - /// - /// This includes all files, directories, and filesystem overhead, - /// but may not account for reserved space depending on the filesystem. - pub used: i32, - - /// Amount of space available for new data in megabytes. - /// - /// This is the space that can be immediately used for new files - /// and may be less than (total - used) due to filesystem reservations. - pub available: i32, - - /// Percentage of disk space currently in use. - /// - /// Range: 0 to 100, calculated as (used / total) * 100. - /// Values above 90% typically indicate the need for cleanup or expansion. - pub used_percentage: i32, - - /// Mount point or drive letter where the filesystem is accessible. - /// - /// Examples: "/", "/home", "/var", "C:", "D:" - /// This helps identify which part of the system's storage hierarchy - /// this disk information represents. - pub mounted_path: String, -} - -/// Structure for querying system information records from the database. -/// -/// This structure is used when retrieving system monitoring data from the -/// database. It includes the database-generated ID field and can be used -/// for displaying historical monitoring data, generating reports, and -/// API responses. -/// -/// # Usage Patterns -/// -/// - Retrieving recent system performance data for dashboards -/// - Historical analysis and trend reporting -/// - API endpoints that return monitoring data to clients -/// - Data export and backup operations -/// -/// # Examples -/// -/// ```rust -/// use teus::monitor::schema::SysInfo; -/// use diesel::prelude::*; -/// -/// // Query recent system information (pseudo-code) -/// // let recent_data: Vec = sysinfo::table -/// // .order(sysinfo::timestamp.desc()) -/// // .limit(10) -/// // .load(&mut connection)?; -/// ``` -#[derive(Queryable, Selectable, Identifiable, Debug, Serialize, Deserialize)] -#[diesel(table_name = sysinfo)] -pub struct SysInfo { - /// Database-generated unique identifier for this record. - /// - /// This is the primary key and is automatically assigned when - /// the record is inserted into the database. Used for referencing - /// this specific monitoring snapshot. - #[diesel(column_name = id)] - pub id: Option, - - /// Timestamp when this system information was collected. - /// - /// Stored in RFC3339 format for consistent parsing and timezone handling. - pub timestamp: String, - - /// CPU usage percentage at the time of collection. - /// - /// Range: 0.0 to 100.0, representing the overall CPU utilization - /// across all cores and threads. - pub cpu_usage: f32, - - /// Amount of RAM currently in use, in megabytes. - /// - /// Active memory usage excluding cached and buffered memory. - pub ram_usage: f32, - - /// Total amount of RAM available in the system, in megabytes. - /// - /// Physical memory capacity of the system. - pub total_ram: f32, - - /// Amount of RAM currently free and available, in megabytes. - /// - /// Memory immediately available for allocation to new processes. - pub free_ram: f32, - - /// Amount of swap space currently in use, in megabytes. - /// - /// High values may indicate memory pressure and performance issues. - pub used_swap: f32, -} - -/// Structure for querying disk information records from the database. -/// -/// This structure represents stored disk usage information that can be -/// retrieved for historical analysis, reporting, and API responses. -/// It includes the database-generated ID and maintains the relationship -/// to its parent system information record. -/// -/// # Relationships -/// -/// Each `DiskInfo` record is linked to a `SysInfo` record through -/// the `sysinfo_id` foreign key, allowing for comprehensive system -/// monitoring data retrieval. -/// -/// # Common Query Patterns -/// -/// - Retrieving disk usage trends over time -/// - Finding disks approaching capacity limits -/// - Generating storage utilization reports -/// - Monitoring filesystem-specific usage patterns -/// -/// # Examples -/// -/// ```rust -/// use teus::monitor::schema::DiskInfo; -/// use diesel::prelude::*; -/// -/// // Query disk info for high usage (pseudo-code) -/// // let high_usage_disks: Vec = diskinfo::table -/// // .filter(diskinfo::used_percentage.gt(90)) -/// // .load(&mut connection)?; -/// ``` -#[derive(Queryable, Selectable, Identifiable, Debug, Serialize, Deserialize)] -#[diesel(table_name = diskinfo)] -pub struct DiskInfo { - /// Database-generated unique identifier for this disk record. - /// - /// Primary key used for referencing this specific disk monitoring entry. - #[diesel(column_name = id)] - pub id: Option, - - /// Foreign key reference to the associated system information record. - /// - /// Links this disk information to a specific monitoring snapshot, - /// enabling time-series analysis of disk usage. - pub sysinfo_id: i32, - - /// Type of filesystem (e.g., "ext4", "ntfs", "xfs", "btrfs"). - /// - /// Identifies the storage technology and formatting of this disk. - pub filesystem: String, - - /// Total size of the filesystem in megabytes. - /// - /// Total capacity including filesystem overhead and reserved space. - pub size: i32, - - /// Amount of space currently used in megabytes. - /// - /// Space occupied by files, directories, and filesystem metadata. - pub used: i32, - - /// Amount of space available for new data in megabytes. - /// - /// Immediately usable space, may be less than (size - used) - /// due to filesystem reservations and overhead. - pub available: i32, - - /// Percentage of disk space currently in use. - /// - /// Range: 0 to 100, useful for quick assessment of storage pressure. - /// Values above 90% typically require attention. - pub used_percentage: i32, - - /// Mount point or drive letter where the filesystem is accessible. - /// - /// The path in the system's directory hierarchy where this - /// storage device can be accessed (e.g., "/", "/home", "C:"). - pub mounted_path: String, -} - -impl Default for SchemaSysInfo { - fn default() -> Self { - Self { - timestamp: "".to_string(), - cpu_usage: 0.0, - ram_usage: 0.0, - total_ram: 0.0, - free_ram: 0.0, - used_swap: 0.0, - // user_id: 0, - } - } -} - -impl Default for SchemaDiskInfo { - fn default() -> Self { - Self { - sysinfo_id: 0, - filesystem: "".to_string(), - size: 0, - used: 0, - available: 0, - used_percentage: 0, - mounted_path: "".to_string(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - - #[test] - fn test_schema_sys_info_default() { - let sys_info = SchemaSysInfo::default(); - assert_eq!(sys_info.timestamp, ""); - assert_eq!(sys_info.cpu_usage, 0.0); - assert_eq!(sys_info.ram_usage, 0.0); - assert_eq!(sys_info.total_ram, 0.0); - assert_eq!(sys_info.free_ram, 0.0); - assert_eq!(sys_info.used_swap, 0.0); - } - - #[test] - fn test_schema_disk_info_default() { - let disk_info = SchemaDiskInfo::default(); - assert_eq!(disk_info.sysinfo_id, 0); - assert_eq!(disk_info.filesystem, ""); - assert_eq!(disk_info.size, 0); - assert_eq!(disk_info.used, 0); - assert_eq!(disk_info.available, 0); - assert_eq!(disk_info.used_percentage, 0); - assert_eq!(disk_info.mounted_path, ""); - } - - #[test] - fn test_schema_sys_info_creation() { - let sys_info = SchemaSysInfo { - timestamp: Utc::now().to_rfc3339(), - cpu_usage: 25.5, - ram_usage: 1024.0, - total_ram: 8192.0, - free_ram: 4096.0, - used_swap: 512.0, - }; - - assert!(!sys_info.timestamp.is_empty()); - assert_eq!(sys_info.cpu_usage, 25.5); - assert_eq!(sys_info.ram_usage, 1024.0); - assert_eq!(sys_info.total_ram, 8192.0); - assert_eq!(sys_info.free_ram, 4096.0); - assert_eq!(sys_info.used_swap, 512.0); - } - - #[test] - fn test_schema_disk_info_creation() { - let disk_info = SchemaDiskInfo { - sysinfo_id: 1, - filesystem: "ext4".to_string(), - size: 1000, - used: 500, - available: 500, - used_percentage: 50, - mounted_path: "/".to_string(), - }; - - assert_eq!(disk_info.sysinfo_id, 1); - assert_eq!(disk_info.filesystem, "ext4"); - assert_eq!(disk_info.size, 1000); - assert_eq!(disk_info.used, 500); - assert_eq!(disk_info.available, 500); - assert_eq!(disk_info.used_percentage, 50); - assert_eq!(disk_info.mounted_path, "/"); - } - - #[test] - fn test_sys_info_serialization() { - let sys_info = SysInfo { - id: Some(1), - timestamp: Utc::now().to_rfc3339(), - cpu_usage: 25.5, - ram_usage: 1024.0, - total_ram: 8192.0, - free_ram: 4096.0, - used_swap: 512.0, - }; - - let serialized = serde_json::to_string(&sys_info).unwrap(); - assert!(serialized.contains("\"id\":1")); - assert!(serialized.contains("\"cpu_usage\":25.5")); - assert!(serialized.contains("\"ram_usage\":1024")); - } - - #[test] - fn test_disk_info_serialization() { - let disk_info = DiskInfo { - id: Some(1), - sysinfo_id: 1, - filesystem: "ext4".to_string(), - size: 1000, - used: 500, - available: 500, - used_percentage: 50, - mounted_path: "/".to_string(), - }; - - let serialized = serde_json::to_string(&disk_info).unwrap(); - assert!(serialized.contains("\"id\":1")); - assert!(serialized.contains("\"sysinfo_id\":1")); - assert!(serialized.contains("\"filesystem\":\"ext4\"")); - assert!(serialized.contains("\"mounted_path\":\"/\"")); - } - - #[test] - fn test_sys_info_debug_format() { - let sys_info = SysInfo { - id: Some(1), - timestamp: "2024-01-01T00:00:00Z".to_string(), - cpu_usage: 25.5, - ram_usage: 1024.0, - total_ram: 8192.0, - free_ram: 4096.0, - used_swap: 512.0, - }; - - let debug_str = format!("{:?}", sys_info); - assert!(debug_str.contains("SysInfo")); - assert!(debug_str.contains("25.5")); - assert!(debug_str.contains("1024")); - } - - #[test] - fn test_edge_values() { - // Test with extreme values - let sys_info = SchemaSysInfo { - timestamp: "2024-01-01T00:00:00Z".to_string(), - cpu_usage: 100.0, - ram_usage: 0.0, - total_ram: f32::MAX, - free_ram: f32::MAX, - used_swap: 0.0, - }; - - assert_eq!(sys_info.cpu_usage, 100.0); - assert_eq!(sys_info.ram_usage, 0.0); - assert_eq!(sys_info.total_ram, f32::MAX); - assert_eq!(sys_info.free_ram, f32::MAX); - assert_eq!(sys_info.used_swap, 0.0); - - // Test disk info with edge values - let disk_info = SchemaDiskInfo { - sysinfo_id: i32::MAX, - filesystem: "test".to_string(), - size: i32::MAX, - used: 0, - available: i32::MAX, - used_percentage: 100, - mounted_path: "/test".to_string(), - }; - - assert_eq!(disk_info.sysinfo_id, i32::MAX); - assert_eq!(disk_info.size, i32::MAX); - assert_eq!(disk_info.used, 0); - assert_eq!(disk_info.available, i32::MAX); - assert_eq!(disk_info.used_percentage, 100); - } - - #[test] - fn test_deserialization() { - let json_str = r#"{ - "timestamp": "2024-01-01T00:00:00Z", - "cpu_usage": 25.5, - "ram_usage": 1024.0, - "total_ram": 8192.0, - "free_ram": 4096.0, - "used_swap": 512.0 - }"#; - - let sys_info: SchemaSysInfo = serde_json::from_str(json_str).unwrap(); - assert_eq!(sys_info.timestamp, "2024-01-01T00:00:00Z"); - assert_eq!(sys_info.cpu_usage, 25.5); - assert_eq!(sys_info.ram_usage, 1024.0); - } -} diff --git a/teus/monitor/sys.rs b/teus/monitor/sys.rs deleted file mode 100644 index 594ed69..0000000 --- a/teus/monitor/sys.rs +++ /dev/null @@ -1,409 +0,0 @@ -use super::mutation; -use super::schema::{SchemaDiskInfo, SchemaSysInfo}; // Import the Diesel insertable structs -use crate::{config::types::Config, monitor::storage::Storage}; -use chrono::Utc; -use diesel::SqliteConnection; // Import SqliteConnection -use std::{thread, time::Duration}; // Import Mutex -use sysinfo::{Disks, MemoryRefreshKind, System}; - -#[allow(dead_code)] -#[derive(Clone, Debug)] -pub struct DiskInfo { - pub available: usize, - // Add disk-related fields here - pub filesystem: String, - pub mounted_path: String, - pub size: usize, - pub used: usize, - pub used_percentage: usize, -} - -#[allow(dead_code)] -#[derive(Clone, Debug)] -pub struct SysInfo { - #[allow(dead_code)] - pub id: i64, - pub timestamp: String, - pub cpu_usage: f64, - pub ram_usage: f64, - pub total_ram: f64, - pub free_ram: f64, - pub used_swap: f64, - pub disks: Vec, -} - -#[allow(dead_code)] -impl SysInfo { - pub fn new( - cpu_usage: f64, - ram_usage: f64, - total_ram: f64, - free_ram: f64, - used_swap: f64, - disks: Vec, - ) -> Self { - Self { - id: 0, - timestamp: "".to_string(), - cpu_usage, - ram_usage, - total_ram, - free_ram, - used_swap, - disks, - } - } - - // Default constructor - pub fn default() -> Self { - Self { - id: 0, - timestamp: Utc::now().to_rfc3339(), - cpu_usage: 0.0, - ram_usage: 0.0, - total_ram: 0.0, - free_ram: 0.0, - used_swap: 0.0, - disks: vec![DiskInfo { - filesystem: String::new(), - size: 0, - used: 0, - available: 0, - used_percentage: 0, - mounted_path: String::new(), - }], - } - } - - pub fn run_monitor(mut self, config: &Config) { - let storage = match Storage::new(&config.database.path) { - Ok(storage) => storage, - Err(e) => { - eprintln!("Failed to create storage: {}", e); - return; - } - }; - - // Get a mutable connection from the Arc> - let mut conn_guard = match storage.diesel_conn.lock() { - Ok(guard) => guard, - Err(poisoned) => { - eprintln!("Failed to acquire lock on DB connection: {}", poisoned); - // Handle the poisoned mutex appropriately, maybe panic or return - return; - } - }; - // Dereference the guard to get the &mut SqliteConnection - let conn: &mut SqliteConnection = &mut *conn_guard; - - let mut sys = System::new_all(); - let disks_sysinfo = Disks::new_with_refreshed_list(); // Renamed to avoid conflict - - sys.refresh_all(); - sys.refresh_memory_specifics(MemoryRefreshKind::nothing().with_ram()); - - self.total_ram = sys.total_memory() as f64; - self.free_ram = sys.free_memory() as f64; - self.used_swap = sys.used_swap() as f64; - self.ram_usage = sys.used_memory() as f64; // Use used_memory for ram_usage - - thread::sleep(Duration::from_millis(250)); - sys.refresh_cpu_all(); - - let cpu_count = sys.cpus().len(); - let total_cpu_usage: f64 = sys.cpus().iter().map(|cpu| cpu.cpu_usage() as f64).sum(); - self.cpu_usage = if cpu_count > 0 { - total_cpu_usage / cpu_count as f64 - } else { - 0.0 - }; - - // --- Prepare data for Diesel insertion --- - self.timestamp = Utc::now().to_rfc3339(); // Ensure timestamp is current - - // Create the SchemaSysInfo struct for insertion - let new_sys_info_to_insert = SchemaSysInfo { - timestamp: self.timestamp, - cpu_usage: self.cpu_usage as f32, // Cast f64 to f32 - ram_usage: self.ram_usage as f32, // Cast f64 to f32 - total_ram: self.total_ram as f32, // Cast f64 to f32 - free_ram: self.free_ram as f32, // Cast f64 to f32 - used_swap: self.used_swap as f32, // Cast f64 to f32 - }; - - // Insert system info using the SchemaSysInfo struct - let sysinfo_id = match mutation::insert_sysinfo(conn, &new_sys_info_to_insert) { - Ok(id) => id, - Err(e) => { - eprintln!("Failed to insert system info: {}", e); - // Drop the lock before returning - drop(conn_guard); - return; - } - }; - - // Prepare disk info data for batch insertion - let mut disk_infos_to_insert: Vec = Vec::new(); - for disk in disks_sysinfo.list() { - let space_used = disk.total_space() - disk.available_space(); - // Calculate usage percentage correctly - let usage_percentage = if disk.total_space() > 0 { - (space_used as f64 / disk.total_space() as f64 * 100.0) as i32 - } else { - 0 - }; - - let fs_name = disk.name().to_string_lossy().to_string(); - let mount_point = disk.mount_point().to_string_lossy().to_string(); - - disk_infos_to_insert.push(SchemaDiskInfo { - sysinfo_id, // Use the ID from the inserted sysinfo - filesystem: fs_name, - size: (disk.total_space() / 1024 / 1024) as i32, // Convert bytes to MB (adjust if needed) and cast usize to i32 - used: (space_used / 1024 / 1024) as i32, // Convert bytes to MB and cast usize to i32 - available: (disk.available_space() / 1024 / 1024) as i32, // Convert bytes to MB and cast usize to i32 - used_percentage: usage_percentage, // Use calculated percentage - mounted_path: mount_point, - }); - } - - // Insert disk info using the SchemaDiskInfo structs - if !disk_infos_to_insert.is_empty() { - if let Err(e) = mutation::insert_multiple_diskinfo(conn, &disk_infos_to_insert) { - eprintln!("Failed to insert disk info batch: {}", e); - } - } - // Lock is automatically dropped here when conn_guard goes out of scope - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::types::{Config, DatabaseConfig, Environment, MonitorConfig, ServerConfig}; - - #[allow(dead_code)] - fn create_test_config() -> Config { - Config { - server: ServerConfig { - host: "127.0.0.1".to_string(), - port: 8080, - secret: "test_secret".to_string(), - environment: Environment::Test, - }, - database: DatabaseConfig { - path: ":memory:".to_string(), - }, - monitor: MonitorConfig { interval_secs: 60 }, - } - } - - #[test] - fn test_disk_info_creation() { - let disk_info = DiskInfo { - available: 1000, - filesystem: "ext4".to_string(), - mounted_path: "/".to_string(), - size: 2000, - used: 1000, - used_percentage: 50, - }; - - assert_eq!(disk_info.available, 1000); - assert_eq!(disk_info.filesystem, "ext4"); - assert_eq!(disk_info.mounted_path, "/"); - assert_eq!(disk_info.size, 2000); - assert_eq!(disk_info.used, 1000); - assert_eq!(disk_info.used_percentage, 50); - } - - #[test] - fn test_disk_info_clone() { - let disk_info = DiskInfo { - available: 500, - filesystem: "ntfs".to_string(), - mounted_path: "C:\\".to_string(), - size: 1000, - used: 500, - used_percentage: 50, - }; - - let cloned = disk_info.clone(); - assert_eq!(disk_info.available, cloned.available); - assert_eq!(disk_info.filesystem, cloned.filesystem); - assert_eq!(disk_info.mounted_path, cloned.mounted_path); - assert_eq!(disk_info.size, cloned.size); - assert_eq!(disk_info.used, cloned.used); - assert_eq!(disk_info.used_percentage, cloned.used_percentage); - } - - #[test] - fn test_sysinfo_new() { - let disks = vec![DiskInfo { - available: 1000, - filesystem: "ext4".to_string(), - mounted_path: "/".to_string(), - size: 2000, - used: 1000, - used_percentage: 50, - }]; - - let sysinfo = SysInfo::new(25.5, 8000.0, 16000.0, 8000.0, 2000.0, disks.clone()); - - assert_eq!(sysinfo.id, 0); - assert_eq!(sysinfo.timestamp, ""); - assert_eq!(sysinfo.cpu_usage, 25.5); - assert_eq!(sysinfo.ram_usage, 8000.0); - assert_eq!(sysinfo.total_ram, 16000.0); - assert_eq!(sysinfo.free_ram, 8000.0); - assert_eq!(sysinfo.used_swap, 2000.0); - assert_eq!(sysinfo.disks.len(), 1); - assert_eq!(sysinfo.disks[0].filesystem, "ext4"); - } - - #[test] - fn test_sysinfo_default() { - let sysinfo = SysInfo::default(); - - assert_eq!(sysinfo.id, 0); - assert!(!sysinfo.timestamp.is_empty()); // Should have a timestamp - assert_eq!(sysinfo.cpu_usage, 0.0); - assert_eq!(sysinfo.ram_usage, 0.0); - assert_eq!(sysinfo.total_ram, 0.0); - assert_eq!(sysinfo.free_ram, 0.0); - assert_eq!(sysinfo.used_swap, 0.0); - assert_eq!(sysinfo.disks.len(), 1); - assert_eq!(sysinfo.disks[0].filesystem, ""); - assert_eq!(sysinfo.disks[0].mounted_path, ""); - assert_eq!(sysinfo.disks[0].size, 0); - assert_eq!(sysinfo.disks[0].used, 0); - assert_eq!(sysinfo.disks[0].available, 0); - assert_eq!(sysinfo.disks[0].used_percentage, 0); - } - - #[test] - fn test_sysinfo_clone() { - let disks = vec![DiskInfo { - available: 2000, - filesystem: "btrfs".to_string(), - mounted_path: "/home".to_string(), - size: 4000, - used: 2000, - used_percentage: 50, - }]; - - let sysinfo = SysInfo::new(15.7, 4000.0, 8000.0, 4000.0, 1000.0, disks); - - let cloned = sysinfo.clone(); - assert_eq!(sysinfo.id, cloned.id); - assert_eq!(sysinfo.timestamp, cloned.timestamp); - assert_eq!(sysinfo.cpu_usage, cloned.cpu_usage); - assert_eq!(sysinfo.ram_usage, cloned.ram_usage); - assert_eq!(sysinfo.total_ram, cloned.total_ram); - assert_eq!(sysinfo.free_ram, cloned.free_ram); - assert_eq!(sysinfo.used_swap, cloned.used_swap); - assert_eq!(sysinfo.disks.len(), cloned.disks.len()); - assert_eq!(sysinfo.disks[0].filesystem, cloned.disks[0].filesystem); - } - - #[test] - fn test_sysinfo_debug_format() { - let sysinfo = SysInfo::default(); - let debug_str = format!("{:?}", sysinfo); - assert!(debug_str.contains("SysInfo")); - assert!(debug_str.contains("cpu_usage")); - assert!(debug_str.contains("ram_usage")); - } - - #[test] - fn test_disk_info_debug_format() { - let disk_info = DiskInfo { - available: 1000, - filesystem: "ext4".to_string(), - mounted_path: "/".to_string(), - size: 2000, - used: 1000, - used_percentage: 50, - }; - - let debug_str = format!("{:?}", disk_info); - assert!(debug_str.contains("DiskInfo")); - assert!(debug_str.contains("ext4")); - assert!(debug_str.contains("1000")); - } - - #[test] - fn test_sysinfo_edge_values() { - let disks = vec![]; - - // Test with extreme values - let sysinfo = SysInfo::new( - 100.0, // Max CPU usage - 0.0, // No RAM usage - u64::MAX as f64, // Maximum possible RAM - u64::MAX as f64, // Maximum free RAM - 0.0, // No swap usage - disks, - ); - - assert_eq!(sysinfo.cpu_usage, 100.0); - assert_eq!(sysinfo.ram_usage, 0.0); - assert_eq!(sysinfo.total_ram, u64::MAX as f64); - assert_eq!(sysinfo.free_ram, u64::MAX as f64); - assert_eq!(sysinfo.used_swap, 0.0); - assert_eq!(sysinfo.disks.len(), 0); - } - - #[test] - fn test_disk_info_percentage_calculation() { - // Test 100% usage - let full_disk = DiskInfo { - available: 0, - filesystem: "ext4".to_string(), - mounted_path: "/".to_string(), - size: 1000, - used: 1000, - used_percentage: 100, - }; - assert_eq!(full_disk.used_percentage, 100); - - // Test 0% usage - let empty_disk = DiskInfo { - available: 1000, - filesystem: "ext4".to_string(), - mounted_path: "/".to_string(), - size: 1000, - used: 0, - used_percentage: 0, - }; - assert_eq!(empty_disk.used_percentage, 0); - } - - #[test] - fn test_sysinfo_with_multiple_disks() { - let disks = vec![ - DiskInfo { - available: 1000, - filesystem: "ext4".to_string(), - mounted_path: "/".to_string(), - size: 2000, - used: 1000, - used_percentage: 50, - }, - DiskInfo { - available: 500, - filesystem: "ext4".to_string(), - mounted_path: "/home".to_string(), - size: 1000, - used: 500, - used_percentage: 50, - }, - ]; - - let sysinfo = SysInfo::new(30.0, 4000.0, 8000.0, 4000.0, 1000.0, disks); - - assert_eq!(sysinfo.disks.len(), 2); - assert_eq!(sysinfo.disks[0].mounted_path, "/"); - assert_eq!(sysinfo.disks[1].mounted_path, "/home"); - } -} From d4fcb4b83a9fde46c3318f1a1be1d4c0e89cc2a8 Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:01:43 +0200 Subject: [PATCH 07/21] Remove webserver directory and endpoints The webserver directory and its endpoints are being removed. This appears to be a major architectural change, likely moving these components elsewhere or replacing them with a different implementation. --- teus/webserver/api.rs | 117 -------- teus/webserver/auth/handlers.rs | 398 -------------------------- teus/webserver/auth/middleware.rs | 235 --------------- teus/webserver/auth/mod.rs | 5 - teus/webserver/auth/mutation.rs | 39 --- teus/webserver/auth/query.rs | 16 -- teus/webserver/auth/schema.rs | 127 -------- teus/webserver/docker/handlers.rs | 84 ------ teus/webserver/docker/mod.rs | 1 - teus/webserver/mod.rs | 4 - teus/webserver/services/mod.rs | 1 - teus/webserver/services/systeminfo.rs | 82 ------ 12 files changed, 1109 deletions(-) delete mode 100644 teus/webserver/api.rs delete mode 100644 teus/webserver/auth/handlers.rs delete mode 100644 teus/webserver/auth/middleware.rs delete mode 100644 teus/webserver/auth/mod.rs delete mode 100644 teus/webserver/auth/mutation.rs delete mode 100644 teus/webserver/auth/query.rs delete mode 100644 teus/webserver/auth/schema.rs delete mode 100644 teus/webserver/docker/handlers.rs delete mode 100644 teus/webserver/docker/mod.rs delete mode 100644 teus/webserver/mod.rs delete mode 100644 teus/webserver/services/mod.rs delete mode 100644 teus/webserver/services/systeminfo.rs diff --git a/teus/webserver/api.rs b/teus/webserver/api.rs deleted file mode 100644 index 55c2d85..0000000 --- a/teus/webserver/api.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::bookmarks::handlers as bookmark_handlers; -use crate::config::handlers::get_teus_config; -use crate::monitor::query; -use crate::webserver::auth::handlers::{check, login, signup, JwtConfig}; -use crate::webserver::auth::middleware::AuthMiddlewareFactory; -use crate::webserver::docker::handlers::{ - get_docker_container, get_docker_containers, get_docker_version, get_docker_volume, - get_docker_volumes, -}; -use crate::webserver::services::systeminfo; -use crate::monitor::storage::Storage; -use teus_types::api_models::{DiskInfoResponse, SysInfoResponse}; -use teus_types::config::Config; -use actix_cors::Cors; -use actix_web::error::ErrorInternalServerError; -use actix_web::{get, http, middleware, web, App, Error, HttpResponse, HttpServer}; - -// TODO: move this api into another file `syshandler` or something -#[get("/sysinfo")] -async fn sysinfo_handler(storage: web::Data) -> Result { - let mut conn = storage.diesel_conn.lock().map_err(|_| { - eprintln!("Mutex poisoned while getting sysinfo"); // TODO: Use log::error! - ErrorInternalServerError("Failed to acquire database lock") - })?; - - let sys_info_result = query::get_latest_sysinfo_with_disks(&mut conn).map_err(|e| { - eprintln!("Database error getting sysinfo: {:?}", e); // TODO: Use log::error! - ErrorInternalServerError("Failed to get latest sysinfo") - })?; - - if let Some((sys_info, disks)) = sys_info_result { - let timestamp = sys_info.timestamp.clone(); - let disks = disks - .iter() - .map(|d| DiskInfoResponse { - filesystem: d.filesystem.clone(), - mount_point: d.mounted_path.clone(), - total_space: d.size, - available_space: d.available, - used_space: d.used, - }) - .collect(); - - let response = SysInfoResponse { - timestamp, - cpu_usage: sys_info.cpu_usage, - ram_usage: sys_info.ram_usage, - total_ram: sys_info.total_ram, - free_ram: sys_info.free_ram, - used_swap: sys_info.used_swap, - disks: disks, - }; - - Ok(HttpResponse::Ok().json(response)) - } else { - Ok(HttpResponse::NotFound().json("No sysinfo found")) - } -} - -#[actix_web::main] -pub async fn start_webserver(config: &Config, storage: Storage) -> std::io::Result<()> { - let url = format!("{}:{}", config.server.host, config.server.port); - println!("Webserver listening on {}", url); - - let app_config_data = web::Data::new(config.clone()); - let app_storage_data = web::Data::new(storage.clone()); // Clone for app_data - - // TODO: Put the secret here from the config - let jwt_secret = config.server.secret.clone(); - let jwt_config = web::Data::new(JwtConfig { - secret: jwt_secret.to_string(), - expiration_hours: 24, - }); - - HttpServer::new(move || { - let cors = Cors::default() - .allow_any_origin() - .allowed_methods(vec!["GET", "POST", "DELETE", "PATCH"]) - .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) - .allowed_header(http::header::CONTENT_TYPE) - .max_age(3600); - - App::new() - .wrap(middleware::Logger::default()) - .wrap(cors) - .app_data(app_config_data.clone()) // Share config - .app_data(app_storage_data.clone()) // Share storage - .app_data(jwt_config.clone()) // Share JWT config - // Public routes - .service( - web::scope("/api/v1/auth") - .service(login) - .service(signup) - .service(get_teus_config), - ) - // Protected routes - .service( - web::scope("/api/v1/teus") - .wrap(AuthMiddlewareFactory::new(jwt_secret.to_string())) - .service(check) // check for auth - .service(sysinfo_handler) - .service(systeminfo::get_sysinfo) - .service(get_docker_version) - .service(get_docker_containers) - .service(get_docker_container) - .service(get_docker_volume) - .service(get_docker_volumes) - .service(bookmark_handlers::get_user_services) - .service(bookmark_handlers::add_service) - .service(bookmark_handlers::delete_service_by_id) - .service(bookmark_handlers::update_service_by_id), - ) - }) - .bind(&url)? - .run() - .await -} diff --git a/teus/webserver/auth/handlers.rs b/teus/webserver/auth/handlers.rs deleted file mode 100644 index e99c0f7..0000000 --- a/teus/webserver/auth/handlers.rs +++ /dev/null @@ -1,398 +0,0 @@ -use crate::{ - config::schema::TeusConfig, - monitor::storage::Storage, - webserver::auth::{middleware::Claims, schema::User}, -}; -use teus_types::config::Config; -use actix_web::{post, web, HttpResponse, Responder}; -use argon2::{ - password_hash::{PasswordHash, PasswordVerifier}, - Argon2, -}; -use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header}; -use serde::{Deserialize, Serialize}; - -/// Request structure for user authentication login endpoint. -/// -/// This structure represents the data required for a user to authenticate -/// with the Teus system. It contains the credentials that will be validated -/// against the stored user data in the database. -/// -/// # Security Considerations -/// -/// - The password field should be transmitted over HTTPS in production -/// - Passwords are never logged or stored in plain text -/// - Consider implementing rate limiting to prevent brute force attacks -/// -/// # Examples -/// -/// JSON request body: -/// ```json -/// { -/// "username": "admin", -/// "password": "secure_password" -/// } -/// ``` -/// -/// # API Endpoint -/// -/// Used with `POST /auth/login` -#[derive(Deserialize)] -pub struct LoginRequest { - /// The user's login identifier. - /// - /// This should be a unique username that exists in the system. - /// Case-sensitive matching is performed against stored usernames. - username: String, - - /// The user's password in plain text. - /// - /// This will be verified against the stored password hash using - /// Argon2 password hashing. The plain text password is never - /// stored or logged by the system. - password: String, -} - -/// Request structure for user registration endpoint. -/// -/// This structure represents the data required to create a new user -/// account in the Teus system. The provided password will be securely -/// hashed using Argon2 before storage. -/// -/// # Validation Requirements -/// -/// - Username must be unique in the system -/// - Password should meet minimum security requirements (implemented at application level) -/// - Both fields are required and cannot be empty -/// -/// # Security Features -/// -/// - Passwords are hashed with Argon2 and a unique salt -/// - Plain text passwords are never stored -/// - Username uniqueness is enforced at the database level -/// -/// # Examples -/// -/// JSON request body: -/// ```json -/// { -/// "username": "newuser", -/// "password": "secure_password123" -/// } -/// ``` -/// -/// # API Endpoint -/// -/// Used with `POST /auth/signup` -#[derive(Deserialize)] -pub struct SignupRequest { - /// The desired username for the new account. - /// - /// Must be unique across all users in the system. If a user - /// with this username already exists, the registration will fail - /// with a 409 Conflict status. - username: String, - - /// The password for the new account in plain text. - /// - /// This will be securely hashed using Argon2 with a unique salt - /// before being stored in the database. The plain text password - /// is never persisted. - password: String, -} - -/// Response structure for successful authentication operations. -/// -/// This structure contains the JWT tokens issued after successful login -/// or token refresh operations. It provides both access and refresh tokens -/// following OAuth 2.0-style token patterns. -/// -/// # Token Types -/// -/// - **Access Token**: Short-lived token for API authentication (default: configurable hours) -/// - **Refresh Token**: Long-lived token for obtaining new access tokens (default: 7 days) -/// -/// # Security Notes -/// -/// - Access tokens should be stored securely on the client (e.g., memory, secure storage) -/// - Refresh tokens should be stored even more securely and used only for token renewal -/// - Both tokens are JWTs signed with the server's secret key -/// -/// # Examples -/// -/// JSON response: -/// ```json -/// { -/// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", -/// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", -/// "expires_in": 3600 -/// } -/// ``` -#[derive(Serialize)] -pub struct TokenResponse { - /// JWT access token for API authentication. - /// - /// This token should be included in the Authorization header - /// of subsequent API requests as "Bearer {token}". It has a - /// shorter expiration time for security. - access: String, - - /// JWT refresh token for obtaining new access tokens. - /// - /// This token can be used to obtain a new access token when - /// the current one expires, without requiring the user to - /// log in again. It has a longer expiration time. - refresh: String, - - /// Time until the access token expires, in seconds. - /// - /// Clients should use this value to determine when to refresh - /// the access token using the refresh token. This is typically - /// calculated as expiration_hours * 3600. - expires_in: i64, -} - -/// Configuration structure for JWT token generation and validation. -/// -/// This structure holds the configuration parameters needed for -/// JSON Web Token operations in the authentication system. It's -/// typically initialized once at application startup. -/// -/// # Security Considerations -/// -/// - The secret should be cryptographically secure and sufficiently long -/// - The secret should be different for each environment (dev, test, prod) -/// - Consider rotating secrets periodically in production environments -/// - Never log or expose the secret in error messages -/// -/// # Examples -/// -/// ```rust -/// use teus::webserver::auth::handlers::JwtConfig; -/// -/// let jwt_config = JwtConfig { -/// secret: "your-256-bit-secret-key".to_string(), -/// expiration_hours: 1, // 1 hour for access tokens -/// }; -/// ``` -pub struct JwtConfig { - /// Secret key used for signing and verifying JWT tokens. - /// - /// This should be a cryptographically secure random string, - /// at least 256 bits (32 characters) long. The same secret - /// must be used for both token generation and validation. - pub secret: String, - - /// Number of hours until access tokens expire. - /// - /// Shorter expiration times improve security by reducing the - /// window of opportunity if a token is compromised, but may - /// require more frequent token refreshes. Typical values - /// range from 1-24 hours. - pub expiration_hours: i64, -} - -/// Generic response structure for simple API messages. -/// -/// This structure is used for API endpoints that need to return -/// a simple text message, such as error responses or confirmation -/// messages. It provides a consistent format for client applications. -/// -/// # Usage Patterns -/// -/// - Error messages (e.g., "Invalid credentials", "User not found") -/// - Success confirmations (e.g., "Operation completed") -/// - Validation errors (e.g., "Username already exists") -/// -/// # Examples -/// -/// JSON response: -/// ```json -/// { -/// "message": "Invalid credentials" -/// } -/// ``` -/// -/// # TODO -/// -/// Consider moving this to a separate common response module -/// to be shared across different handlers and avoid duplication. -#[derive(Serialize)] -struct GenericResponse { - /// The message content to be returned to the client. - /// - /// Should be user-friendly and provide clear information - /// about the result of the operation or the nature of any error. - message: String, -} - -/// Response structure for successful user registration. -/// -/// This structure contains the essential information about a newly -/// created user account, returned after successful signup operations. -/// It excludes sensitive information like passwords and salts. -/// -/// # Security Notes -/// -/// - Only non-sensitive user information is included -/// - Password hashes and salts are never included in responses -/// - The user ID can be used for subsequent API operations -/// -/// # Examples -/// -/// JSON response: -/// ```json -/// { -/// "id": 1, -/// "username": "newuser" -/// } -/// ``` -/// -/// # API Endpoint -/// -/// Returned by `POST /auth/signup` on successful user creation -#[derive(Serialize)] -struct NewUserResponse { - /// The database-generated unique identifier for the new user. - /// - /// This ID is used internally by the system to reference - /// the user in database relations and API operations. - id: i32, - - /// The username of the newly created account. - /// - /// Confirms the username that was successfully registered, - /// useful for client-side confirmation and user feedback. - username: String, -} - -// Verify the password against the hash -fn is_same_password(password_hash: &str, clear_pass: &str, _salt: &str) -> bool { - // Try to parse the password hash - let parsed_hash = match PasswordHash::new(password_hash) { - Ok(hash) => hash, - Err(_) => return false, - }; - - // Verify the password against the hash - Argon2::default() - .verify_password(clear_pass.as_bytes(), &parsed_hash) - .is_ok() -} - -// Handler di login -#[post("/login")] -pub async fn login( - login_data: web::Json, - jwt_config: web::Data, - config: actix_web::web::Data, -) -> impl Responder { - let storage = Storage::new(&config.database.path).unwrap(); - let mut conn = storage.diesel_conn.lock().unwrap(); - let user = User::find_by_username(&mut *conn, &login_data.username).unwrap(); - let user_id: i32; - - match user { - Some(user) => { - println!("User found: {:?}", user); - let is_password_correct = - is_same_password(&user.password, &login_data.password, &user.salt); - - if !is_password_correct { - let response = GenericResponse { - message: "Invalid Credentials".to_string(), - }; - return HttpResponse::Unauthorized().json(response); - } - - user_id = user.id.unwrap(); - } - None => { - println!("User not found"); - let response = GenericResponse { - message: "Invalid Credentials".to_string(), - }; - return HttpResponse::Unauthorized().json(response); - } - } - - let access_expiration = Utc::now() - .checked_add_signed(Duration::hours(jwt_config.expiration_hours)) - .expect("Valid timestamp") - .timestamp() as usize; - - let refresh_expiration = Utc::now() - .checked_add_signed(Duration::hours(24 * 7)) // 7 days - .expect("Valid timestamp") - .timestamp() as usize; - - let access_claims = Claims { - sub: login_data.username.clone(), - exp: access_expiration, - iat: Utc::now().timestamp() as usize, - id: user_id, - }; - - let refresh_claims = Claims { - sub: login_data.username.clone(), - exp: refresh_expiration, - iat: Utc::now().timestamp() as usize, - id: user_id, - }; - - let access_token = encode( - &Header::default(), - &access_claims, - &EncodingKey::from_secret(jwt_config.secret.as_bytes()), - ) - .unwrap(); - - let refresh_token = encode( - &Header::default(), - &refresh_claims, - &EncodingKey::from_secret(jwt_config.secret.as_bytes()), - ) - .unwrap(); - - let response = TokenResponse { - access: access_token, - refresh: refresh_token, - expires_in: jwt_config.expiration_hours * 3600, - }; - - HttpResponse::Ok().json(response) -} - -#[post("/signup")] -pub async fn signup( - signup_data: web::Json, - config: actix_web::web::Data, -) -> impl Responder { - let storage = Storage::new(&config.database.path).unwrap(); - let mut conn = storage.diesel_conn.lock().unwrap(); - - let existing_user = User::find_by_username(&mut *conn, &signup_data.username).unwrap(); - if existing_user.is_some() { - let response = GenericResponse { - message: "Username already exists".to_string(), - }; - return HttpResponse::Conflict().json(response); - } - let user = User::create(&mut *conn, &signup_data.username, &signup_data.password).unwrap(); - TeusConfig::set_first_visit(&mut *conn, false).unwrap(); - - // Create a response without the sensitive data - let user_response = NewUserResponse { - id: user.id.unwrap(), - username: user.username, - }; - - HttpResponse::Created().json(user_response) -} - -// TODO: Move this `health` somewhere else -#[post("/check")] -pub async fn check() -> impl Responder { - HttpResponse::Ok() -} diff --git a/teus/webserver/auth/middleware.rs b/teus/webserver/auth/middleware.rs deleted file mode 100644 index e0e46f0..0000000 --- a/teus/webserver/auth/middleware.rs +++ /dev/null @@ -1,235 +0,0 @@ -//! Authentication middleware for JWT token validation. -//! -//! This module provides middleware components for validating JWT tokens -//! in HTTP requests. It automatically extracts and validates Bearer tokens -//! from the Authorization header, making user claims available to handlers. - -use actix_web::{ - dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, - error::ErrorUnauthorized, - Error, HttpMessage, -}; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; -use serde::{Deserialize, Serialize}; -use std::{ - future::{ready, Future, Ready}, - pin::Pin, - rc::Rc, -}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -struct NotAuth { - message: String, -} - -/// JWT claims structure containing user authentication information. -/// -/// This structure represents the payload of a JSON Web Token (JWT) used -/// for user authentication in the Teus system. It follows JWT standard -/// claims with additional application-specific fields. -/// -/// # Standard JWT Claims -/// -/// - `sub` (Subject): Identifies the user the token was issued for -/// - `exp` (Expiration): Unix timestamp when the token expires -/// - `iat` (Issued At): Unix timestamp when the token was created -/// -/// # Custom Claims -/// -/// - `id`: Numeric user ID for database operations -/// -/// # Security Considerations -/// -/// - Tokens should be validated for expiration before use -/// - The signing key must be kept secure and consistent -/// - Claims should not contain sensitive information -/// -/// # Examples -/// -/// ```rust -/// use teus::webserver::auth::middleware::Claims; -/// use chrono::Utc; -/// -/// let claims = Claims { -/// sub: "admin".to_string(), -/// exp: (Utc::now().timestamp() + 3600) as usize, // 1 hour from now -/// iat: Utc::now().timestamp() as usize, -/// id: 1, -/// }; -/// ``` -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Claims { - /// Subject - the username of the authenticated user. - /// - /// This field identifies which user the token belongs to and - /// corresponds to the username stored in the database. - pub sub: String, - - /// Expiration time as a Unix timestamp. - /// - /// Tokens should be rejected if the current time is after - /// this timestamp. This prevents indefinite token reuse. - pub exp: usize, - - /// Issued at time as a Unix timestamp. - /// - /// Records when the token was created, useful for token - /// lifecycle management and security auditing. - pub iat: usize, - - /// Numeric user ID from the database. - /// - /// This provides a direct reference to the user record - /// for efficient database lookups in authenticated endpoints. - pub id: i32, -} - -/// Factory for creating authentication middleware instances. -/// -/// This factory is responsible for creating `AuthMiddleware` instances -/// with the proper JWT secret configuration. It implements the Actix-web -/// `Transform` trait to integrate with the web framework's middleware system. -/// -/// # Usage -/// -/// The factory is typically registered once during application startup -/// and automatically creates middleware instances for each request scope. -/// -/// # Examples -/// -/// ```rust -/// use actix_web::App; -/// use teus::webserver::auth::middleware::AuthMiddlewareFactory; -/// -/// let app = App::new() -/// .wrap(AuthMiddlewareFactory::new("your-jwt-secret".to_string())); -/// ``` -pub struct AuthMiddlewareFactory { - /// The secret key used for JWT token validation. - /// - /// This must match the secret used when generating tokens. - /// Should be cryptographically secure and kept confidential. - jwt_secret: String, -} - -impl AuthMiddlewareFactory { - /// Creates a new authentication middleware factory. - /// - /// # Arguments - /// - /// * `jwt_secret` - The secret key for JWT token validation - /// - /// # Examples - /// - /// ```rust - /// use teus::webserver::auth::middleware::AuthMiddlewareFactory; - /// - /// let factory = AuthMiddlewareFactory::new("secure-secret-key".to_string()); - /// ``` - pub fn new(jwt_secret: String) -> Self { - Self { jwt_secret } - } -} - -/// Authentication middleware that validates JWT tokens in HTTP requests. -/// -/// This middleware automatically extracts Bearer tokens from the Authorization -/// header, validates them using the configured JWT secret, and makes the user -/// claims available to downstream handlers through request extensions. -/// -/// # Request Processing -/// -/// 1. Extracts the Authorization header from the request -/// 2. Validates the Bearer token format -/// 3. Decodes and validates the JWT using the secret key -/// 4. Injects the claims into request extensions for handler access -/// 5. Returns 401 Unauthorized for invalid or missing tokens -/// -/// # Token Format -/// -/// The middleware expects tokens in the standard Bearer format: -/// ``` -/// Authorization: Bearer -/// ``` -/// -/// # Error Handling -/// -/// Returns `401 Unauthorized` for: -/// - Missing Authorization header -/// - Invalid header format (not starting with "Bearer ") -/// - Invalid or expired JWT tokens -/// - Tokens signed with a different secret -pub struct AuthMiddleware { - /// The wrapped service to call after successful authentication. - service: Rc, - - /// The JWT secret key for token validation. - jwt_secret: String, -} - -impl Transform for AuthMiddlewareFactory -where - S: Service, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Transform = AuthMiddleware; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(AuthMiddleware { - service: Rc::new(service), - jwt_secret: self.jwt_secret.clone(), - })) - } -} - -impl Service for AuthMiddleware -where - S: Service, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = Pin>>>; - - forward_ready!(service); - - fn call(&self, req: ServiceRequest) -> Self::Future { - let service = self.service.clone(); - let jwt_secret = self.jwt_secret.clone(); - - Box::pin(async move { - let auth_header = req.headers().get("Authorization"); - let token = match auth_header { - Some(header) => { - let header_str = header - .to_str() - .map_err(|_| ErrorUnauthorized("Invalid Authorization header format"))?; - - if !header_str.starts_with("Bearer ") { - return Err(ErrorUnauthorized("Invalid Authorization header format")); - } - - header_str[7..].trim() - } - None => return Err(ErrorUnauthorized("Authorization header missing")), - }; - - let token_data = decode::( - token, - &DecodingKey::from_secret(jwt_secret.as_bytes()), - &Validation::new(Algorithm::HS256), - ) - .map_err(|_| ErrorUnauthorized("Invalid token"))?; - - let claims = token_data.claims; - req.extensions_mut().insert(claims.clone()); - service.call(req).await - }) - } -} diff --git a/teus/webserver/auth/mod.rs b/teus/webserver/auth/mod.rs deleted file mode 100644 index f3ef93f..0000000 --- a/teus/webserver/auth/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod handlers; -pub mod middleware; -pub mod mutation; -pub mod query; -pub mod schema; diff --git a/teus/webserver/auth/mutation.rs b/teus/webserver/auth/mutation.rs deleted file mode 100644 index 6fe84c3..0000000 --- a/teus/webserver/auth/mutation.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::schema::User; -use argon2::password_hash::rand_core::OsRng; -use argon2::password_hash::SaltString; -use argon2::{Argon2, PasswordHasher}; -use diesel::result::Error; -use diesel::{RunQueryDsl, SqliteConnection}; - -impl User { - pub fn create( - conn: &mut SqliteConnection, - username: &str, - password: &str, - ) -> Result { - use crate::schema::user; - - // Get a random salt for the password - let salt = SaltString::generate(&mut OsRng); - - // Create an Argon2 instance with default parameters - let argon2 = Argon2::default(); - - // Generate the password hash as a string - let password_hash = argon2 - .hash_password(password.as_bytes(), &salt) - .unwrap() - .to_string(); - - let new_user = User { - id: None, - username: username.to_string(), - password: password_hash, - salt: salt.as_str().to_owned(), - }; - - diesel::insert_into(user::table) - .values(&new_user) - .get_result(conn) - } -} diff --git a/teus/webserver/auth/query.rs b/teus/webserver/auth/query.rs deleted file mode 100644 index a6ab817..0000000 --- a/teus/webserver/auth/query.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::schema::User; -use diesel::{prelude::*, result::Error}; - -impl User { - pub fn find_by_username( - conn: &mut SqliteConnection, - username_q: &str, - ) -> Result, Error> { - use crate::schema::user::dsl::*; - - user.filter(username.eq(username_q)) - .select(User::as_select()) - .first(conn) - .optional() - } -} diff --git a/teus/webserver/auth/schema.rs b/teus/webserver/auth/schema.rs deleted file mode 100644 index a4676ef..0000000 --- a/teus/webserver/auth/schema.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! User authentication schema structures. -//! -//! This module defines the database schema structure for user accounts -//! in the Teus system. It handles user authentication data including -//! secure password storage with Argon2 hashing and salt generation. - -use diesel::prelude::*; -use serde::Serialize; - -/// Database schema structure for user accounts. -/// -/// This structure represents user authentication data stored in the SQLite -/// database. It includes all necessary fields for secure user management -/// including password hashing and salt storage. -/// -/// # Security Features -/// -/// - Passwords are stored as Argon2 hashes, never in plain text -/// - Each password has a unique salt to prevent rainbow table attacks -/// - Username uniqueness is enforced at the database level -/// - User IDs are auto-generated and used for session management -/// -/// # Database Operations -/// -/// This structure supports both insertion (user creation) and querying -/// (user lookup) operations through Diesel ORM. The `Insertable` trait -/// allows new user creation, while `Queryable` enables data retrieval. -/// -/// # JSON Serialization -/// -/// The structure can be serialized to JSON for API responses, but note -/// that password hashes and salts should typically be excluded from -/// client-facing responses for security reasons. -/// -/// # Examples -/// -/// Creating a new user (handled by implementation methods): -/// ```rust -/// use teus::webserver::auth::schema::User; -/// // User creation is handled by User::create() method with proper hashing -/// ``` -/// -/// # Related Modules -/// -/// - `mutation.rs`: Contains `User::create()` for user registration -/// - `query.rs`: Contains `User::find_by_username()` for authentication -/// - `handlers.rs`: Uses this structure in login/signup endpoints -#[derive(Insertable, Queryable, Selectable, Serialize, Debug)] -#[diesel(table_name = crate::schema::user)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct User { - /// Database-generated unique identifier for the user. - /// - /// This is the primary key and is automatically assigned when - /// a new user is created. Used for session management and - /// referencing the user in JWT tokens and other operations. - /// - /// # Database Behavior - /// - /// - `None` for new users before insertion - /// - `Some(id)` for existing users retrieved from database - pub id: Option, - - /// Unique username for the user account. - /// - /// This serves as the primary login identifier and must be unique - /// across all users in the system. Username matching is case-sensitive. - /// - /// # Constraints - /// - /// - Must be unique (enforced by database) - /// - Cannot be empty or null - /// - Used for login authentication - /// - /// # Security Notes - /// - /// - Safe to include in API responses and logs - /// - Used as the JWT token subject (`sub` claim) - pub username: String, - - /// Argon2 password hash for secure authentication. - /// - /// This field stores the hashed representation of the user's password - /// using the Argon2 algorithm with a unique salt. The original password - /// is never stored in the database. - /// - /// # Security Features - /// - /// - Uses Argon2id variant for maximum security - /// - Includes salt, iteration count, and memory parameters - /// - Resistant to rainbow table and brute force attacks - /// - Format follows PHC string format for compatibility - /// - /// # Important Notes - /// - /// - **NEVER** include this field in API responses - /// - **NEVER** log this field value - /// - Only used for password verification during login - /// - Updated only when user changes their password - pub password: String, - - /// Cryptographic salt used for password hashing. - /// - /// This field stores the unique salt that was used when hashing - /// the user's password. Each user has a different salt to prevent - /// rainbow table attacks and ensure password hash uniqueness. - /// - /// # Security Properties - /// - /// - Cryptographically random and unique per user - /// - Generated using secure random number generation - /// - Combined with password before hashing - /// - Stored separately but used during verification - /// - /// # Implementation Details - /// - /// - Generated using `SaltString::generate()` from the `argon2` crate - /// - Base64-encoded string format for database storage - /// - Required for Argon2 password verification process - /// - /// # Security Warning - /// - /// Like the password hash, this field should **NEVER** be included - /// in API responses or logged, as it could potentially aid in - /// password cracking attempts. - pub salt: String, -} diff --git a/teus/webserver/docker/handlers.rs b/teus/webserver/docker/handlers.rs deleted file mode 100644 index 0e802e8..0000000 --- a/teus/webserver/docker/handlers.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::fmt::Debug; - -use actix_web::{get, web, HttpResponse, Responder}; -use docker::docker::DockerClient; -use serde::{Deserialize, Serialize}; -use serde_qs::to_string; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GenericDockerResponse { - message: String, -} - -#[derive(Debug, Deserialize, Serialize)] -struct ContainersQuery { - all: Option, -} - -#[get("/docker/version")] -async fn get_docker_version() -> impl Responder { - let mut docker_client = DockerClient::new(None); - match docker_client.get_version() { - Ok(version) => HttpResponse::Ok().json(version), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker version: {:?}", err), - }), - } -} - -#[get("/docker/containers")] -async fn get_docker_containers(query: web::Query) -> impl Responder { - println!("Query: {:?}", query); - - let query_params: ContainersQuery = query.into_inner(); - let query_string = to_string(&query_params).unwrap(); - - println!("Query: {:?}", query_string); - let mut docker_client = DockerClient::new(None); - - match docker_client.get_containers(Some(query_string)) { - Ok(containers) => HttpResponse::Ok().json(containers), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker containers: {:?}", err), - }), - } -} - -#[get("/docker/container/{id}")] -async fn get_docker_container(id: web::Path) -> impl Responder { - let mut docker_client = DockerClient::new(None); - let container_id_clone = id.clone(); - - match docker_client.get_container_details(container_id_clone) { - Ok(container) => HttpResponse::Ok().json(container), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker container {}: {:?}", id, err), - }), - } -} - -#[get("/docker/volumes")] -async fn get_docker_volumes() -> impl Responder { - let mut docker_client = DockerClient::new(None); - match docker_client.get_volumes() { - Ok(volumes) => HttpResponse::Ok().json(volumes), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker volumes: {:?}", err), - }), - } -} - -// FIXME: The id is not being passed correctly -// I think the problem is in the enum DockerApi -#[get("/docker/volumes/{id}")] -async fn get_docker_volume(id: web::Path) -> impl Responder { - let mut docker_client = DockerClient::new(None); - - let cloned_id = id.clone(); - match docker_client.get_volume_details(cloned_id) { - Ok(volume) => HttpResponse::Ok().json(volume), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker volume {} details: {:?}", id, err), - }), - } -} diff --git a/teus/webserver/docker/mod.rs b/teus/webserver/docker/mod.rs deleted file mode 100644 index c3d4495..0000000 --- a/teus/webserver/docker/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod handlers; diff --git a/teus/webserver/mod.rs b/teus/webserver/mod.rs deleted file mode 100644 index 726d9ae..0000000 --- a/teus/webserver/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod api; -pub mod auth; -pub mod docker; -pub mod services; diff --git a/teus/webserver/services/mod.rs b/teus/webserver/services/mod.rs deleted file mode 100644 index 2d24348..0000000 --- a/teus/webserver/services/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod systeminfo; diff --git a/teus/webserver/services/systeminfo.rs b/teus/webserver/services/systeminfo.rs deleted file mode 100644 index b1e0b6b..0000000 --- a/teus/webserver/services/systeminfo.rs +++ /dev/null @@ -1,82 +0,0 @@ -use teus_types::api_models::{GenericSysInfoResponse, IpInfo, MACInfo}; -use actix_web::{get, HttpResponse, Responder}; -use sysinfo::{Networks, System}; - -fn collect_network_info() -> Vec { - let networks = Networks::new_with_refreshed_list(); - let mut ip_structs = Vec::new(); - - for (interface_name, network) in &networks { - for ip_network in network.ip_networks() { - ip_structs.push(IpInfo { - interface: interface_name.to_string(), - addr: ip_network.addr.to_string(), - prefix: ip_network.prefix, - }); - } - } - - ip_structs -} - -fn collect_mac_address() -> Vec { - let networks = Networks::new_with_refreshed_list(); - let mut mac_addresses = Vec::new(); - for (interface_name, network) in &networks { - mac_addresses.push(MACInfo { - interface: interface_name.to_string(), - mac: network.mac_address().to_string(), - }); - } - mac_addresses -} - -fn convert_seconds_to_date_time(seconds: u64) -> String { - let minutes = seconds / 60; - let hours = minutes / 60; - let days = hours / 24; - - format!( - "{} days, {} hours, {} minutes", - days, - hours % 24, - minutes % 60 - ) -} - -fn get_os_name_version() -> String { - let os = System::long_os_version(); - match os { - Some(version) => version, - None => "No Info".to_string(), - } -} - -/// Returns system information such as hostname, OS details, and network configurations. -/// -/// This endpoint provides real-time system information that doesn't need to be stored -/// in the database, as it can be queried directly from the operating system whenever needed. -/// The information returned is transient and reflects the current state of the system -/// rather than persistent data that needs database storage. -#[get("/generic/sysinfo")] -async fn get_sysinfo() -> impl Responder { - let hostname = System::host_name().unwrap_or_else(|| "No Info".to_string()); - let networks = collect_network_info(); - let mac_addresses = collect_mac_address(); - let os_name = get_os_name_version(); - - let uptime_ms = System::uptime(); - let uptime = convert_seconds_to_date_time(uptime_ms); - - let mut response = GenericSysInfoResponse::default(); - - response.hostname = hostname; - response.os = os_name; - response.uptime = uptime; - response.kernel_version = System::kernel_version().unwrap_or_else(|| "No Info".to_string()); - response.ipv4 = "No Info".to_string(); - response.networks = networks; - response.mac_addresses = mac_addresses; - - HttpResponse::Ok().json(response) -} From afee52430908628fe2525a8b9f660593ecf4cbfe Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:02:03 +0200 Subject: [PATCH 08/21] Split modules into separate crates and refactor imports --- teus/lib.rs | 4 ---- teus/main.rs | 13 +++++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/teus/lib.rs b/teus/lib.rs index 14fdc1a..7ab37a3 100644 --- a/teus/lib.rs +++ b/teus/lib.rs @@ -1,6 +1,2 @@ -pub mod bookmarks; -pub mod config; -pub mod monitor; pub mod schema; pub mod utils; -pub mod webserver; diff --git a/teus/main.rs b/teus/main.rs index 7275b0a..b48766d 100644 --- a/teus/main.rs +++ b/teus/main.rs @@ -1,6 +1,7 @@ -use teus::{config, monitor, webserver}; - -use monitor::sys::SysInfo; +use teus_config::config; +use teus_database::storage; +use teus_api::routes; +use teus_monitor::sys::SysInfo; use std::{ env, path::Path, @@ -37,7 +38,7 @@ fn main() { }; // Initialize Storage once - let storage = match monitor::storage::Storage::new(&config.database.path) { + let storage = match storage::Storage::new(&config.database.path) { Ok(s) => s, Err(e) => { eprintln!("Failed to initialize storage: {}", e); @@ -59,8 +60,8 @@ fn main() { let config_clone_for_web = config.clone(); // Clone config for webserver let storage_clone_for_web = storage.clone(); // Clone storage for webserver let web_handle_thread = thread::spawn(move || { - // This will run the webserver in a separate thread - let _ = webserver::api::start_webserver(&config_clone_for_web, storage_clone_for_web); + /* TODO: consider a RC instead to avoid cloning */ + let _ = routes::start_webserver(&config_clone_for_web, storage_clone_for_web); }); // Give the webserver a moment to start From d3eb073524ade375b46886d64094a5370d642641 Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:02:23 +0200 Subject: [PATCH 09/21] Add new API crate with sysinfo and route handlers --- crates/teus-api/Cargo.toml | 16 +++ crates/teus-api/src/handlers/mod.rs | 1 + crates/teus-api/src/handlers/systeminfo.rs | 83 ++++++++++++++ crates/teus-api/src/lib.rs | 2 + crates/teus-api/src/routes.rs | 120 +++++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 crates/teus-api/Cargo.toml create mode 100644 crates/teus-api/src/handlers/mod.rs create mode 100644 crates/teus-api/src/handlers/systeminfo.rs create mode 100644 crates/teus-api/src/lib.rs create mode 100644 crates/teus-api/src/routes.rs diff --git a/crates/teus-api/Cargo.toml b/crates/teus-api/Cargo.toml new file mode 100644 index 0000000..57f59e5 --- /dev/null +++ b/crates/teus-api/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "teus-api" +version = "0.1.0" +edition = "2024" + +[dependencies] +teus-types = { path = "../teus-types" } +teus-auth = { path = "../teus-auth" } +teus-monitor = { path = "../teus-monitor" } +teus-docker = { path = "../teus-docker" } +teus-database = { path = "../teus-database"} +teus-config = { path = "../teus-config"} +teus-services = { path = "../teus-services" } +actix-web = "4.9" +actix-cors = "0.7" +sysinfo = "0.33.1" diff --git a/crates/teus-api/src/handlers/mod.rs b/crates/teus-api/src/handlers/mod.rs new file mode 100644 index 0000000..2d24348 --- /dev/null +++ b/crates/teus-api/src/handlers/mod.rs @@ -0,0 +1 @@ +pub mod systeminfo; diff --git a/crates/teus-api/src/handlers/systeminfo.rs b/crates/teus-api/src/handlers/systeminfo.rs new file mode 100644 index 0000000..c659c91 --- /dev/null +++ b/crates/teus-api/src/handlers/systeminfo.rs @@ -0,0 +1,83 @@ +use teus_types::api_models::{GenericSysInfoResponse, IpInfo, MACInfo}; +use actix_web::{get, HttpResponse, Responder}; +use sysinfo::{Networks, System}; + +/* TODO: Migrate those services into teus-services crate */ +fn collect_network_info() -> Vec { + let networks = Networks::new_with_refreshed_list(); + let mut ip_structs = Vec::new(); + + for (interface_name, network) in &networks { + for ip_network in network.ip_networks() { + ip_structs.push(IpInfo { + interface: interface_name.to_string(), + addr: ip_network.addr.to_string(), + prefix: ip_network.prefix, + }); + } + } + + ip_structs +} + +fn collect_mac_address() -> Vec { + let networks = Networks::new_with_refreshed_list(); + let mut mac_addresses = Vec::new(); + for (interface_name, network) in &networks { + mac_addresses.push(MACInfo { + interface: interface_name.to_string(), + mac: network.mac_address().to_string(), + }); + } + mac_addresses +} + +fn convert_seconds_to_date_time(seconds: u64) -> String { + let minutes = seconds / 60; + let hours = minutes / 60; + let days = hours / 24; + + format!( + "{} days, {} hours, {} minutes", + days, + hours % 24, + minutes % 60 + ) +} + +fn get_os_name_version() -> String { + let os = System::long_os_version(); + match os { + Some(version) => version, + None => "No Info".to_string(), + } +} + +/// Returns system information such as hostname, OS details, and network configurations. +/// +/// This endpoint provides real-time system information that doesn't need to be stored +/// in the database, as it can be queried directly from the operating system whenever needed. +/// The information returned is transient and reflects the current state of the system +/// rather than persistent data that needs database storage. +#[get("/generic/sysinfo")] +async fn get_sysinfo() -> impl Responder { + let hostname = System::host_name().unwrap_or_else(|| "No Info".to_string()); + let networks = collect_network_info(); + let mac_addresses = collect_mac_address(); + let os_name = get_os_name_version(); + + let uptime_ms = System::uptime(); + let uptime = convert_seconds_to_date_time(uptime_ms); + + let mut response = GenericSysInfoResponse::default(); + + response.hostname = hostname; + response.os = os_name; + response.uptime = uptime; + response.kernel_version = System::kernel_version().unwrap_or_else(|| "No Info".to_string()); + response.ipv4 = "No Info".to_string(); + response.networks = networks; + response.mac_addresses = mac_addresses; + + HttpResponse::Ok().json(response) +} diff --git a/crates/teus-api/src/lib.rs b/crates/teus-api/src/lib.rs new file mode 100644 index 0000000..dd38790 --- /dev/null +++ b/crates/teus-api/src/lib.rs @@ -0,0 +1,2 @@ +pub mod routes; +pub mod handlers; diff --git a/crates/teus-api/src/routes.rs b/crates/teus-api/src/routes.rs new file mode 100644 index 0000000..a5d1be2 --- /dev/null +++ b/crates/teus-api/src/routes.rs @@ -0,0 +1,120 @@ +/* REMOVE SERVICES FROM THIS CRATE */ +/* THIS CRATE IS ONLY FOR HANDLE THE ACTIX WEBSERVICE */ + +use crate::handlers::systeminfo; +use actix_cors::Cors; +use actix_web::error::ErrorInternalServerError; +use actix_web::{App, Error, HttpResponse, HttpServer, get, http, middleware, web}; +use teus_auth::handlers::{JwtConfig, check, login, signup}; +use teus_auth::middleware::AuthMiddlewareFactory; +use teus_config::config::handlers::get_teus_config; +use teus_database::storage::Storage; +use teus_docker::handlers::{ + get_docker_container, get_docker_containers, get_docker_version, get_docker_volume, + get_docker_volumes, +}; +use teus_monitor::query; +use teus_services::bookmarks::handlers as bookmark_handlers; +use teus_types::api_models::{DiskInfoResponse, SysInfoResponse}; +use teus_types::config::Config; + +// TODO: move this api into another file `syshandler` or something +#[get("/sysinfo")] +async fn sysinfo_handler(storage: web::Data) -> Result { + let mut conn = storage.diesel_conn.lock().map_err(|_| { + eprintln!("Mutex poisoned while getting sysinfo"); // TODO: Use log::error! + ErrorInternalServerError("Failed to acquire database lock") + })?; + + let sys_info_result = query::get_latest_sysinfo_with_disks(&mut conn).map_err(|e| { + eprintln!("Database error getting sysinfo: {:?}", e); // TODO: Use log::error! + ErrorInternalServerError("Failed to get latest sysinfo") + })?; + + if let Some((sys_info, disks)) = sys_info_result { + let timestamp = sys_info.timestamp.clone(); + let disks = disks + .iter() + .map(|d| DiskInfoResponse { + filesystem: d.filesystem.clone(), + mount_point: d.mounted_path.clone(), + total_space: d.size, + available_space: d.available, + used_space: d.used, + }) + .collect(); + + let response = SysInfoResponse { + timestamp, + cpu_usage: sys_info.cpu_usage, + ram_usage: sys_info.ram_usage, + total_ram: sys_info.total_ram, + free_ram: sys_info.free_ram, + used_swap: sys_info.used_swap, + disks: disks, + }; + + Ok(HttpResponse::Ok().json(response)) + } else { + Ok(HttpResponse::NotFound().json("No sysinfo found")) + } +} + +#[actix_web::main] +pub async fn start_webserver(config: &Config, storage: Storage) -> std::io::Result<()> { + let url = format!("{}:{}", config.server.host, config.server.port); + println!("Webserver listening on {}", url); + + let app_config_data = web::Data::new(config.clone()); + let app_storage_data = web::Data::new(storage.clone()); // Clone for app_data + + // TODO: Put the secret here from the config + let jwt_secret = config.server.secret.clone(); + let jwt_config = web::Data::new(JwtConfig { + secret: jwt_secret.to_string(), + expiration_hours: 24, + }); + + HttpServer::new(move || { + let cors = Cors::default() + .allow_any_origin() + .allowed_methods(vec!["GET", "POST", "DELETE", "PATCH"]) + .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) + .allowed_header(http::header::CONTENT_TYPE) + .max_age(3600); + + App::new() + .wrap(middleware::Logger::default()) + .wrap(cors) + .app_data(app_config_data.clone()) // Share config + .app_data(app_storage_data.clone()) // Share storage + .app_data(jwt_config.clone()) // Share JWT config + // Public routes + .service( + web::scope("/api/v1/auth") + .service(login) + .service(signup) + .service(get_teus_config), + ) + // Protected routes + .service( + web::scope("/api/v1/teus") + .wrap(AuthMiddlewareFactory::new(jwt_secret.to_string())) + .service(check) // check for auth + .service(sysinfo_handler) + .service(systeminfo::get_sysinfo) + .service(get_docker_version) + .service(get_docker_containers) + .service(get_docker_container) + .service(get_docker_volume) + .service(get_docker_volumes) + .service(bookmark_handlers::get_user_services) + .service(bookmark_handlers::add_service) + .service(bookmark_handlers::delete_service_by_id) + .service(bookmark_handlers::update_service_by_id), + ) + }) + .bind(&url)? + .run() + .await +} From 56416251941aabaea7a6ce5a204d742e764469ec Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:02:41 +0200 Subject: [PATCH 10/21] Add authentication module with JWT token support The code adds a complete authentication module with JWT token support, user registration, and login functionality. It includes password hashing with Argon2, middleware for JWT validation, and secure token generation. The auth module handles: - User signup with secure password storage - Login with JWT token generation - Token validation middleware - Database schema for user accounts --- crates/teus-auth/Cargo.toml | 19 ++ crates/teus-auth/src/handlers.rs | 397 +++++++++++++++++++++++++++++ crates/teus-auth/src/lib.rs | 5 + crates/teus-auth/src/middleware.rs | 235 +++++++++++++++++ crates/teus-auth/src/mutation.rs | 39 +++ crates/teus-auth/src/query.rs | 16 ++ crates/teus-auth/src/schema.rs | 128 ++++++++++ 7 files changed, 839 insertions(+) create mode 100644 crates/teus-auth/Cargo.toml create mode 100644 crates/teus-auth/src/handlers.rs create mode 100644 crates/teus-auth/src/lib.rs create mode 100644 crates/teus-auth/src/middleware.rs create mode 100644 crates/teus-auth/src/mutation.rs create mode 100644 crates/teus-auth/src/query.rs create mode 100644 crates/teus-auth/src/schema.rs diff --git a/crates/teus-auth/Cargo.toml b/crates/teus-auth/Cargo.toml new file mode 100644 index 0000000..90ea1d8 --- /dev/null +++ b/crates/teus-auth/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "teus-auth" +version = "0.1.0" +edition = "2024" + +[dependencies] +teus-types = { path = "../teus-types" } +teus-database = { path = "../teus-database" } +teus-config = { path = "../teus-config" } +teus-schema = { path = "../teus-schema" } +actix-web = "4.9" +jsonwebtoken = "9.3" +argon2 = "0.5" +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1.0.218", features = ["derive"] } +diesel = { version = "2.2.0", features = [ + "sqlite", + "returning_clauses_for_sqlite_3_35", +] } diff --git a/crates/teus-auth/src/handlers.rs b/crates/teus-auth/src/handlers.rs new file mode 100644 index 0000000..404cd57 --- /dev/null +++ b/crates/teus-auth/src/handlers.rs @@ -0,0 +1,397 @@ +use teus_database::storage::Storage; +use crate::middleware::Claims; +use crate::schema::User; +use teus_config::config::schema::TeusConfig; +use teus_types::config::Config; +use actix_web::{post, web, HttpResponse, Responder}; +use argon2::{ + password_hash::{PasswordHash, PasswordVerifier}, + Argon2, +}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Deserialize, Serialize}; + +/// Request structure for user authentication login endpoint. +/// +/// This structure represents the data required for a user to authenticate +/// with the Teus system. It contains the credentials that will be validated +/// against the stored user data in the database. +/// +/// # Security Considerations +/// +/// - The password field should be transmitted over HTTPS in production +/// - Passwords are never logged or stored in plain text +/// - Consider implementing rate limiting to prevent brute force attacks +/// +/// # Examples +/// +/// JSON request body: +/// ```json +/// { +/// "username": "admin", +/// "password": "secure_password" +/// } +/// ``` +/// +/// # API Endpoint +/// +/// Used with `POST /auth/login` +#[derive(Deserialize)] +pub struct LoginRequest { + /// The user's login identifier. + /// + /// This should be a unique username that exists in the system. + /// Case-sensitive matching is performed against stored usernames. + username: String, + + /// The user's password in plain text. + /// + /// This will be verified against the stored password hash using + /// Argon2 password hashing. The plain text password is never + /// stored or logged by the system. + password: String, +} + +/// Request structure for user registration endpoint. +/// +/// This structure represents the data required to create a new user +/// account in the Teus system. The provided password will be securely +/// hashed using Argon2 before storage. +/// +/// # Validation Requirements +/// +/// - Username must be unique in the system +/// - Password should meet minimum security requirements (implemented at application level) +/// - Both fields are required and cannot be empty +/// +/// # Security Features +/// +/// - Passwords are hashed with Argon2 and a unique salt +/// - Plain text passwords are never stored +/// - Username uniqueness is enforced at the database level +/// +/// # Examples +/// +/// JSON request body: +/// ```json +/// { +/// "username": "newuser", +/// "password": "secure_password123" +/// } +/// ``` +/// +/// # API Endpoint +/// +/// Used with `POST /auth/signup` +#[derive(Deserialize)] +pub struct SignupRequest { + /// The desired username for the new account. + /// + /// Must be unique across all users in the system. If a user + /// with this username already exists, the registration will fail + /// with a 409 Conflict status. + username: String, + + /// The password for the new account in plain text. + /// + /// This will be securely hashed using Argon2 with a unique salt + /// before being stored in the database. The plain text password + /// is never persisted. + password: String, +} + +/// Response structure for successful authentication operations. +/// +/// This structure contains the JWT tokens issued after successful login +/// or token refresh operations. It provides both access and refresh tokens +/// following OAuth 2.0-style token patterns. +/// +/// # Token Types +/// +/// - **Access Token**: Short-lived token for API authentication (default: configurable hours) +/// - **Refresh Token**: Long-lived token for obtaining new access tokens (default: 7 days) +/// +/// # Security Notes +/// +/// - Access tokens should be stored securely on the client (e.g., memory, secure storage) +/// - Refresh tokens should be stored even more securely and used only for token renewal +/// - Both tokens are JWTs signed with the server's secret key +/// +/// # Examples +/// +/// JSON response: +/// ```json +/// { +/// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", +/// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", +/// "expires_in": 3600 +/// } +/// ``` +#[derive(Serialize)] +pub struct TokenResponse { + /// JWT access token for API authentication. + /// + /// This token should be included in the Authorization header + /// of subsequent API requests as "Bearer {token}". It has a + /// shorter expiration time for security. + access: String, + + /// JWT refresh token for obtaining new access tokens. + /// + /// This token can be used to obtain a new access token when + /// the current one expires, without requiring the user to + /// log in again. It has a longer expiration time. + refresh: String, + + /// Time until the access token expires, in seconds. + /// + /// Clients should use this value to determine when to refresh + /// the access token using the refresh token. This is typically + /// calculated as expiration_hours * 3600. + expires_in: i64, +} + +/// Configuration structure for JWT token generation and validation. +/// +/// This structure holds the configuration parameters needed for +/// JSON Web Token operations in the authentication system. It's +/// typically initialized once at application startup. +/// +/// # Security Considerations +/// +/// - The secret should be cryptographically secure and sufficiently long +/// - The secret should be different for each environment (dev, test, prod) +/// - Consider rotating secrets periodically in production environments +/// - Never log or expose the secret in error messages +/// +/// # Examples +/// +/// ```rust +/// use teus::webserver::auth::handlers::JwtConfig; +/// +/// let jwt_config = JwtConfig { +/// secret: "your-256-bit-secret-key".to_string(), +/// expiration_hours: 1, // 1 hour for access tokens +/// }; +/// ``` +pub struct JwtConfig { + /// Secret key used for signing and verifying JWT tokens. + /// + /// This should be a cryptographically secure random string, + /// at least 256 bits (32 characters) long. The same secret + /// must be used for both token generation and validation. + pub secret: String, + + /// Number of hours until access tokens expire. + /// + /// Shorter expiration times improve security by reducing the + /// window of opportunity if a token is compromised, but may + /// require more frequent token refreshes. Typical values + /// range from 1-24 hours. + pub expiration_hours: i64, +} + +/// Generic response structure for simple API messages. +/// +/// This structure is used for API endpoints that need to return +/// a simple text message, such as error responses or confirmation +/// messages. It provides a consistent format for client applications. +/// +/// # Usage Patterns +/// +/// - Error messages (e.g., "Invalid credentials", "User not found") +/// - Success confirmations (e.g., "Operation completed") +/// - Validation errors (e.g., "Username already exists") +/// +/// # Examples +/// +/// JSON response: +/// ```json +/// { +/// "message": "Invalid credentials" +/// } +/// ``` +/// +/// # TODO +/// +/// Consider moving this to a separate common response module +/// to be shared across different handlers and avoid duplication. +#[derive(Serialize)] +struct GenericResponse { + /// The message content to be returned to the client. + /// + /// Should be user-friendly and provide clear information + /// about the result of the operation or the nature of any error. + message: String, +} + +/// Response structure for successful user registration. +/// +/// This structure contains the essential information about a newly +/// created user account, returned after successful signup operations. +/// It excludes sensitive information like passwords and salts. +/// +/// # Security Notes +/// +/// - Only non-sensitive user information is included +/// - Password hashes and salts are never included in responses +/// - The user ID can be used for subsequent API operations +/// +/// # Examples +/// +/// JSON response: +/// ```json +/// { +/// "id": 1, +/// "username": "newuser" +/// } +/// ``` +/// +/// # API Endpoint +/// +/// Returned by `POST /auth/signup` on successful user creation +#[derive(Serialize)] +struct NewUserResponse { + /// The database-generated unique identifier for the new user. + /// + /// This ID is used internally by the system to reference + /// the user in database relations and API operations. + id: i32, + + /// The username of the newly created account. + /// + /// Confirms the username that was successfully registered, + /// useful for client-side confirmation and user feedback. + username: String, +} + +// Verify the password against the hash +fn is_same_password(password_hash: &str, clear_pass: &str, _salt: &str) -> bool { + // Try to parse the password hash + let parsed_hash = match PasswordHash::new(password_hash) { + Ok(hash) => hash, + Err(_) => return false, + }; + + // Verify the password against the hash + Argon2::default() + .verify_password(clear_pass.as_bytes(), &parsed_hash) + .is_ok() +} + +// Handler di login +#[post("/login")] +pub async fn login( + login_data: web::Json, + jwt_config: web::Data, + config: actix_web::web::Data, +) -> impl Responder { + let storage = Storage::new(&config.database.path).unwrap(); + let mut conn = storage.diesel_conn.lock().unwrap(); + let user = User::find_by_username(&mut *conn, &login_data.username).unwrap(); + let user_id: i32; + + match user { + Some(user) => { + println!("User found: {:?}", user); + let is_password_correct = + is_same_password(&user.password, &login_data.password, &user.salt); + + if !is_password_correct { + let response = GenericResponse { + message: "Invalid Credentials".to_string(), + }; + return HttpResponse::Unauthorized().json(response); + } + + user_id = user.id.unwrap(); + } + None => { + println!("User not found"); + let response = GenericResponse { + message: "Invalid Credentials".to_string(), + }; + return HttpResponse::Unauthorized().json(response); + } + } + + let access_expiration = Utc::now() + .checked_add_signed(Duration::hours(jwt_config.expiration_hours)) + .expect("Valid timestamp") + .timestamp() as usize; + + let refresh_expiration = Utc::now() + .checked_add_signed(Duration::hours(24 * 7)) // 7 days + .expect("Valid timestamp") + .timestamp() as usize; + + let access_claims = Claims { + sub: login_data.username.clone(), + exp: access_expiration, + iat: Utc::now().timestamp() as usize, + id: user_id, + }; + + let refresh_claims = Claims { + sub: login_data.username.clone(), + exp: refresh_expiration, + iat: Utc::now().timestamp() as usize, + id: user_id, + }; + + let access_token = encode( + &Header::default(), + &access_claims, + &EncodingKey::from_secret(jwt_config.secret.as_bytes()), + ) + .unwrap(); + + let refresh_token = encode( + &Header::default(), + &refresh_claims, + &EncodingKey::from_secret(jwt_config.secret.as_bytes()), + ) + .unwrap(); + + let response = TokenResponse { + access: access_token, + refresh: refresh_token, + expires_in: jwt_config.expiration_hours * 3600, + }; + + HttpResponse::Ok().json(response) +} + +#[post("/signup")] +pub async fn signup( + signup_data: web::Json, + config: actix_web::web::Data, +) -> impl Responder { + let storage = Storage::new(&config.database.path).unwrap(); + let mut conn = storage.diesel_conn.lock().unwrap(); + + let existing_user = User::find_by_username(&mut *conn, &signup_data.username).unwrap(); + if existing_user.is_some() { + let response = GenericResponse { + message: "Username already exists".to_string(), + }; + return HttpResponse::Conflict().json(response); + } + let user = User::create(&mut *conn, &signup_data.username, &signup_data.password).unwrap(); + TeusConfig::set_first_visit(&mut *conn, false).unwrap(); + + // Create a response without the sensitive data + let user_response = NewUserResponse { + id: user.id.unwrap(), + username: user.username, + }; + + HttpResponse::Created().json(user_response) +} + +// TODO: Move this `health` somewhere else +#[post("/check")] +pub async fn check() -> impl Responder { + HttpResponse::Ok() +} diff --git a/crates/teus-auth/src/lib.rs b/crates/teus-auth/src/lib.rs new file mode 100644 index 0000000..f3ef93f --- /dev/null +++ b/crates/teus-auth/src/lib.rs @@ -0,0 +1,5 @@ +pub mod handlers; +pub mod middleware; +pub mod mutation; +pub mod query; +pub mod schema; diff --git a/crates/teus-auth/src/middleware.rs b/crates/teus-auth/src/middleware.rs new file mode 100644 index 0000000..e0e46f0 --- /dev/null +++ b/crates/teus-auth/src/middleware.rs @@ -0,0 +1,235 @@ +//! Authentication middleware for JWT token validation. +//! +//! This module provides middleware components for validating JWT tokens +//! in HTTP requests. It automatically extracts and validates Bearer tokens +//! from the Authorization header, making user claims available to handlers. + +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + error::ErrorUnauthorized, + Error, HttpMessage, +}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use serde::{Deserialize, Serialize}; +use std::{ + future::{ready, Future, Ready}, + pin::Pin, + rc::Rc, +}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct NotAuth { + message: String, +} + +/// JWT claims structure containing user authentication information. +/// +/// This structure represents the payload of a JSON Web Token (JWT) used +/// for user authentication in the Teus system. It follows JWT standard +/// claims with additional application-specific fields. +/// +/// # Standard JWT Claims +/// +/// - `sub` (Subject): Identifies the user the token was issued for +/// - `exp` (Expiration): Unix timestamp when the token expires +/// - `iat` (Issued At): Unix timestamp when the token was created +/// +/// # Custom Claims +/// +/// - `id`: Numeric user ID for database operations +/// +/// # Security Considerations +/// +/// - Tokens should be validated for expiration before use +/// - The signing key must be kept secure and consistent +/// - Claims should not contain sensitive information +/// +/// # Examples +/// +/// ```rust +/// use teus::webserver::auth::middleware::Claims; +/// use chrono::Utc; +/// +/// let claims = Claims { +/// sub: "admin".to_string(), +/// exp: (Utc::now().timestamp() + 3600) as usize, // 1 hour from now +/// iat: Utc::now().timestamp() as usize, +/// id: 1, +/// }; +/// ``` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Claims { + /// Subject - the username of the authenticated user. + /// + /// This field identifies which user the token belongs to and + /// corresponds to the username stored in the database. + pub sub: String, + + /// Expiration time as a Unix timestamp. + /// + /// Tokens should be rejected if the current time is after + /// this timestamp. This prevents indefinite token reuse. + pub exp: usize, + + /// Issued at time as a Unix timestamp. + /// + /// Records when the token was created, useful for token + /// lifecycle management and security auditing. + pub iat: usize, + + /// Numeric user ID from the database. + /// + /// This provides a direct reference to the user record + /// for efficient database lookups in authenticated endpoints. + pub id: i32, +} + +/// Factory for creating authentication middleware instances. +/// +/// This factory is responsible for creating `AuthMiddleware` instances +/// with the proper JWT secret configuration. It implements the Actix-web +/// `Transform` trait to integrate with the web framework's middleware system. +/// +/// # Usage +/// +/// The factory is typically registered once during application startup +/// and automatically creates middleware instances for each request scope. +/// +/// # Examples +/// +/// ```rust +/// use actix_web::App; +/// use teus::webserver::auth::middleware::AuthMiddlewareFactory; +/// +/// let app = App::new() +/// .wrap(AuthMiddlewareFactory::new("your-jwt-secret".to_string())); +/// ``` +pub struct AuthMiddlewareFactory { + /// The secret key used for JWT token validation. + /// + /// This must match the secret used when generating tokens. + /// Should be cryptographically secure and kept confidential. + jwt_secret: String, +} + +impl AuthMiddlewareFactory { + /// Creates a new authentication middleware factory. + /// + /// # Arguments + /// + /// * `jwt_secret` - The secret key for JWT token validation + /// + /// # Examples + /// + /// ```rust + /// use teus::webserver::auth::middleware::AuthMiddlewareFactory; + /// + /// let factory = AuthMiddlewareFactory::new("secure-secret-key".to_string()); + /// ``` + pub fn new(jwt_secret: String) -> Self { + Self { jwt_secret } + } +} + +/// Authentication middleware that validates JWT tokens in HTTP requests. +/// +/// This middleware automatically extracts Bearer tokens from the Authorization +/// header, validates them using the configured JWT secret, and makes the user +/// claims available to downstream handlers through request extensions. +/// +/// # Request Processing +/// +/// 1. Extracts the Authorization header from the request +/// 2. Validates the Bearer token format +/// 3. Decodes and validates the JWT using the secret key +/// 4. Injects the claims into request extensions for handler access +/// 5. Returns 401 Unauthorized for invalid or missing tokens +/// +/// # Token Format +/// +/// The middleware expects tokens in the standard Bearer format: +/// ``` +/// Authorization: Bearer +/// ``` +/// +/// # Error Handling +/// +/// Returns `401 Unauthorized` for: +/// - Missing Authorization header +/// - Invalid header format (not starting with "Bearer ") +/// - Invalid or expired JWT tokens +/// - Tokens signed with a different secret +pub struct AuthMiddleware { + /// The wrapped service to call after successful authentication. + service: Rc, + + /// The JWT secret key for token validation. + jwt_secret: String, +} + +impl Transform for AuthMiddlewareFactory +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Transform = AuthMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(AuthMiddleware { + service: Rc::new(service), + jwt_secret: self.jwt_secret.clone(), + })) + } +} + +impl Service for AuthMiddleware +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = Pin>>>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let service = self.service.clone(); + let jwt_secret = self.jwt_secret.clone(); + + Box::pin(async move { + let auth_header = req.headers().get("Authorization"); + let token = match auth_header { + Some(header) => { + let header_str = header + .to_str() + .map_err(|_| ErrorUnauthorized("Invalid Authorization header format"))?; + + if !header_str.starts_with("Bearer ") { + return Err(ErrorUnauthorized("Invalid Authorization header format")); + } + + header_str[7..].trim() + } + None => return Err(ErrorUnauthorized("Authorization header missing")), + }; + + let token_data = decode::( + token, + &DecodingKey::from_secret(jwt_secret.as_bytes()), + &Validation::new(Algorithm::HS256), + ) + .map_err(|_| ErrorUnauthorized("Invalid token"))?; + + let claims = token_data.claims; + req.extensions_mut().insert(claims.clone()); + service.call(req).await + }) + } +} diff --git a/crates/teus-auth/src/mutation.rs b/crates/teus-auth/src/mutation.rs new file mode 100644 index 0000000..81156e9 --- /dev/null +++ b/crates/teus-auth/src/mutation.rs @@ -0,0 +1,39 @@ +use super::schema::User; +use argon2::password_hash::rand_core::OsRng; +use argon2::password_hash::SaltString; +use argon2::{Argon2, PasswordHasher}; +use diesel::result::Error; +use diesel::{RunQueryDsl, SqliteConnection}; + +impl User { + pub fn create( + conn: &mut SqliteConnection, + username: &str, + password: &str, + ) -> Result { + use teus_schema::schema::user; + + // Get a random salt for the password + let salt = SaltString::generate(&mut OsRng); + + // Create an Argon2 instance with default parameters + let argon2 = Argon2::default(); + + // Generate the password hash as a string + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt) + .unwrap() + .to_string(); + + let new_user = User { + id: None, + username: username.to_string(), + password: password_hash, + salt: salt.as_str().to_owned(), + }; + + diesel::insert_into(user::table) + .values(&new_user) + .get_result(conn) + } +} diff --git a/crates/teus-auth/src/query.rs b/crates/teus-auth/src/query.rs new file mode 100644 index 0000000..4e17a65 --- /dev/null +++ b/crates/teus-auth/src/query.rs @@ -0,0 +1,16 @@ +use super::schema::User; +use diesel::{prelude::*, result::Error}; + +impl User { + pub fn find_by_username( + conn: &mut SqliteConnection, + username_q: &str, + ) -> Result, Error> { + use teus_schema::schema::user::dsl::*; + + user.filter(username.eq(username_q)) + .select(User::as_select()) + .first(conn) + .optional() + } +} diff --git a/crates/teus-auth/src/schema.rs b/crates/teus-auth/src/schema.rs new file mode 100644 index 0000000..05d4c00 --- /dev/null +++ b/crates/teus-auth/src/schema.rs @@ -0,0 +1,128 @@ +//! User authentication schema structures. +//! +//! This module defines the database schema structure for user accounts +//! in the Teus system. It handles user authentication data including +//! secure password storage with Argon2 hashing and salt generation. + +use diesel::prelude::*; +use serde::Serialize; +use teus_schema::schema; + +/// Database schema structure for user accounts. +/// +/// This structure represents user authentication data stored in the SQLite +/// database. It includes all necessary fields for secure user management +/// including password hashing and salt storage. +/// +/// # Security Features +/// +/// - Passwords are stored as Argon2 hashes, never in plain text +/// - Each password has a unique salt to prevent rainbow table attacks +/// - Username uniqueness is enforced at the database level +/// - User IDs are auto-generated and used for session management +/// +/// # Database Operations +/// +/// This structure supports both insertion (user creation) and querying +/// (user lookup) operations through Diesel ORM. The `Insertable` trait +/// allows new user creation, while `Queryable` enables data retrieval. +/// +/// # JSON Serialization +/// +/// The structure can be serialized to JSON for API responses, but note +/// that password hashes and salts should typically be excluded from +/// client-facing responses for security reasons. +/// +/// # Examples +/// +/// Creating a new user (handled by implementation methods): +/// ```rust +/// use teus::webserver::auth::schema::User; +/// // User creation is handled by User::create() method with proper hashing +/// ``` +/// +/// # Related Modules +/// +/// - `mutation.rs`: Contains `User::create()` for user registration +/// - `query.rs`: Contains `User::find_by_username()` for authentication +/// - `handlers.rs`: Uses this structure in login/signup endpoints +#[derive(Insertable, Queryable, Selectable, Serialize, Debug)] +#[diesel(table_name = schema::user)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct User { + /// Database-generated unique identifier for the user. + /// + /// This is the primary key and is automatically assigned when + /// a new user is created. Used for session management and + /// referencing the user in JWT tokens and other operations. + /// + /// # Database Behavior + /// + /// - `None` for new users before insertion + /// - `Some(id)` for existing users retrieved from database + pub id: Option, + + /// Unique username for the user account. + /// + /// This serves as the primary login identifier and must be unique + /// across all users in the system. Username matching is case-sensitive. + /// + /// # Constraints + /// + /// - Must be unique (enforced by database) + /// - Cannot be empty or null + /// - Used for login authentication + /// + /// # Security Notes + /// + /// - Safe to include in API responses and logs + /// - Used as the JWT token subject (`sub` claim) + pub username: String, + + /// Argon2 password hash for secure authentication. + /// + /// This field stores the hashed representation of the user's password + /// using the Argon2 algorithm with a unique salt. The original password + /// is never stored in the database. + /// + /// # Security Features + /// + /// - Uses Argon2id variant for maximum security + /// - Includes salt, iteration count, and memory parameters + /// - Resistant to rainbow table and brute force attacks + /// - Format follows PHC string format for compatibility + /// + /// # Important Notes + /// + /// - **NEVER** include this field in API responses + /// - **NEVER** log this field value + /// - Only used for password verification during login + /// - Updated only when user changes their password + pub password: String, + + /// Cryptographic salt used for password hashing. + /// + /// This field stores the unique salt that was used when hashing + /// the user's password. Each user has a different salt to prevent + /// rainbow table attacks and ensure password hash uniqueness. + /// + /// # Security Properties + /// + /// - Cryptographically random and unique per user + /// - Generated using secure random number generation + /// - Combined with password before hashing + /// - Stored separately but used during verification + /// + /// # Implementation Details + /// + /// - Generated using `SaltString::generate()` from the `argon2` crate + /// - Base64-encoded string format for database storage + /// - Required for Argon2 password verification process + /// + /// # Security Warning + /// + /// Like the password hash, this field should **NEVER** be included + /// in API responses or logged, as it could potentially aid in + /// password cracking attempts. + pub salt: String, +} From 30a4e799cb788ec8585ca8d05a5e4a16b6b21faf Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:03:16 +0200 Subject: [PATCH 11/21] Add Diesel-based SQLite storage implementation --- crates/teus-database/Cargo.toml | 11 ++ crates/teus-database/src/lib.rs | 1 + crates/teus-database/src/storage.rs | 205 ++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 crates/teus-database/Cargo.toml create mode 100644 crates/teus-database/src/lib.rs create mode 100644 crates/teus-database/src/storage.rs diff --git a/crates/teus-database/Cargo.toml b/crates/teus-database/Cargo.toml new file mode 100644 index 0000000..056a08a --- /dev/null +++ b/crates/teus-database/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "teus-database" +version = "0.1.0" +edition = "2024" + +[dependencies] +teus-types = { path = "../teus-types" } +diesel = { version = "2.2.0", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } + +[dev-dependencies] +tempfile = "3.8" diff --git a/crates/teus-database/src/lib.rs b/crates/teus-database/src/lib.rs new file mode 100644 index 0000000..30f61eb --- /dev/null +++ b/crates/teus-database/src/lib.rs @@ -0,0 +1 @@ +pub mod storage; diff --git a/crates/teus-database/src/storage.rs b/crates/teus-database/src/storage.rs new file mode 100644 index 0000000..d0f5213 --- /dev/null +++ b/crates/teus-database/src/storage.rs @@ -0,0 +1,205 @@ +use diesel::connection::SimpleConnection; // Added +use diesel::{Connection as ConnectionDiesel, SqliteConnection}; +use std::error::Error; +use std::path::Path; +use std::sync::{Arc, Mutex}; // Added for Box + +/* to avoid to add diesel dep on crates */ +pub type TeuSQLiteConnection = SqliteConnection; + +#[derive(Clone)] +pub struct Storage { + // pub conn: Arc, // @Info: old Arc reference to don't break the code + pub diesel_conn: Arc>, // @Info: use to test diesel for now +} + +mod storage_utils { + use std::{fs, io, path::Path}; + + pub fn ensure_directory_exists(path: &str) -> io::Result<()> { + let dir_path = Path::new(path); + if !dir_path.exists() { + fs::create_dir_all(dir_path)?; + // Consider using log crate for messages instead of println! + // println!("Directory '{}' created.", path); + } + Ok(()) + } +} + +// TODO: Migrate Connection -> SqliteConnection // This TODO can be removed after this refactor +impl Storage { + pub fn new(db_path: &str) -> Result> { + // Changed return type + if let Some(parent) = Path::new(db_path).parent() { + if let Some(parent_str) = parent.to_str() { + storage_utils::ensure_directory_exists(parent_str)?; // Changed from expect + } + } + + // Removed rusqlite connection logic + + let mut conn_new = SqliteConnection::establish(&db_path)?; // Changed from unwrap_or_else + + // Apply PRAGMAs to Diesel connection + // Note: busy_timeout is set in milliseconds for SQLite PRAGMA + conn_new.batch_execute( + "PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000;", + )?; + + Ok(Self { + diesel_conn: Arc::new(Mutex::new(conn_new)), + }) + } + + // Removed _init_db method +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_storage_new_success() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let db_path = temp_dir.path().join("test.db"); + let db_path_str = db_path.to_str().unwrap(); + + let storage = Storage::new(db_path_str); + assert!(storage.is_ok()); + + let storage = storage.unwrap(); + + // Test that we can acquire the mutex lock + let conn_guard = storage.diesel_conn.lock(); + assert!(conn_guard.is_ok()); + } + + #[test] + fn test_storage_creates_parent_directory() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let nested_path = temp_dir.path().join("nested").join("path").join("test.db"); + let db_path_str = nested_path.to_str().unwrap(); + + let storage = Storage::new(db_path_str); + assert!(storage.is_ok()); + + // Verify the nested directories were created + assert!(temp_dir.path().join("nested").join("path").exists()); + } + + #[test] + fn test_storage_with_memory_database() { + // SQLite in-memory database + let storage = Storage::new(":memory:"); + assert!(storage.is_ok()); + + let storage = storage.unwrap(); + let conn_guard = storage.diesel_conn.lock(); + assert!(conn_guard.is_ok()); + } + + #[test] + fn test_storage_invalid_path() { + // Try to create database in a location that doesn't exist and can't be created + // This might not fail on all systems, but it's worth testing + let invalid_path = "/invalid/path/that/should/not/exist/test.db"; + let storage = Storage::new(invalid_path); + + // On most systems this should fail due to permission issues + // But SQLite might create the path in some cases, so we just ensure it returns a Result + match storage { + Ok(_) => { + // If it succeeds, that's fine too - SQLite is quite permissive + } + Err(_) => { + // This is the expected case for most invalid paths + } + } + } + + #[test] + fn test_storage_clone() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let db_path = temp_dir.path().join("test.db"); + let db_path_str = db_path.to_str().unwrap(); + + let storage = Storage::new(db_path_str).expect("Failed to create storage"); + let cloned_storage = storage.clone(); + + // Both should be able to access the same connection + let conn1 = storage.diesel_conn.lock(); + assert!(conn1.is_ok()); + drop(conn1); // Release lock before trying with clone + + let conn2 = cloned_storage.diesel_conn.lock(); + assert!(conn2.is_ok()); + } + + #[test] + fn test_storage_concurrent_access() { + use std::thread; + use std::time::Duration; + + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let db_path = temp_dir.path().join("test.db"); + let db_path_str = db_path.to_str().unwrap(); + + let storage = Storage::new(db_path_str).expect("Failed to create storage"); + let storage_clone = storage.clone(); + + let handle = thread::spawn(move || { + let conn = storage_clone.diesel_conn.lock(); + assert!(conn.is_ok()); + thread::sleep(Duration::from_millis(10)); + }); + + // Give the thread a moment to start + thread::sleep(Duration::from_millis(5)); + + // This should be able to access after the thread releases the lock + handle.join().expect("Thread panicked"); + + let conn = storage.diesel_conn.lock(); + assert!(conn.is_ok()); + } + + #[test] + fn test_storage_utils_ensure_directory_exists() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let test_path = temp_dir.path().join("test_subdir"); + let test_path_str = test_path.to_str().unwrap(); + + // Directory doesn't exist initially + assert!(!test_path.exists()); + + // Call the utility function + let result = storage_utils::ensure_directory_exists(test_path_str); + assert!(result.is_ok()); + + // Directory should now exist + assert!(test_path.exists()); + assert!(test_path.is_dir()); + + // Calling again on existing directory should also work + let result = storage_utils::ensure_directory_exists(test_path_str); + assert!(result.is_ok()); + } + + #[test] + fn test_storage_utils_invalid_path() { + // Try to create a directory in an invalid location + let result = storage_utils::ensure_directory_exists("/root/invalid/path"); + + // This should fail on most systems due to permission issues + match result { + Ok(_) => { + // In some test environments this might succeed + } + Err(_) => { + // This is the expected case for most systems + } + } + } +} From 29d846e4d2b6815fcb7ea4d87bbf6235f64c94f0 Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:03:44 +0200 Subject: [PATCH 12/21] Add Docker REST API handlers and routes --- crates/teus-docker/Cargo.toml | 13 +++++ crates/teus-docker/src/handlers.rs | 84 ++++++++++++++++++++++++++++++ crates/teus-docker/src/lib.rs | 1 + 3 files changed, 98 insertions(+) create mode 100644 crates/teus-docker/Cargo.toml create mode 100644 crates/teus-docker/src/handlers.rs create mode 100644 crates/teus-docker/src/lib.rs diff --git a/crates/teus-docker/Cargo.toml b/crates/teus-docker/Cargo.toml new file mode 100644 index 0000000..7a4400e --- /dev/null +++ b/crates/teus-docker/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "teus-docker" +version = "0.1.0" +edition = "2024" + +[dependencies] +teus-types = { path = "../teus-types" } +docker = { path = "../docker" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } +actix-web = "4.9" +serde_qs = "0.13" diff --git a/crates/teus-docker/src/handlers.rs b/crates/teus-docker/src/handlers.rs new file mode 100644 index 0000000..0e802e8 --- /dev/null +++ b/crates/teus-docker/src/handlers.rs @@ -0,0 +1,84 @@ +use std::fmt::Debug; + +use actix_web::{get, web, HttpResponse, Responder}; +use docker::docker::DockerClient; +use serde::{Deserialize, Serialize}; +use serde_qs::to_string; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GenericDockerResponse { + message: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ContainersQuery { + all: Option, +} + +#[get("/docker/version")] +async fn get_docker_version() -> impl Responder { + let mut docker_client = DockerClient::new(None); + match docker_client.get_version() { + Ok(version) => HttpResponse::Ok().json(version), + Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { + message: format!("Error getting docker version: {:?}", err), + }), + } +} + +#[get("/docker/containers")] +async fn get_docker_containers(query: web::Query) -> impl Responder { + println!("Query: {:?}", query); + + let query_params: ContainersQuery = query.into_inner(); + let query_string = to_string(&query_params).unwrap(); + + println!("Query: {:?}", query_string); + let mut docker_client = DockerClient::new(None); + + match docker_client.get_containers(Some(query_string)) { + Ok(containers) => HttpResponse::Ok().json(containers), + Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { + message: format!("Error getting docker containers: {:?}", err), + }), + } +} + +#[get("/docker/container/{id}")] +async fn get_docker_container(id: web::Path) -> impl Responder { + let mut docker_client = DockerClient::new(None); + let container_id_clone = id.clone(); + + match docker_client.get_container_details(container_id_clone) { + Ok(container) => HttpResponse::Ok().json(container), + Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { + message: format!("Error getting docker container {}: {:?}", id, err), + }), + } +} + +#[get("/docker/volumes")] +async fn get_docker_volumes() -> impl Responder { + let mut docker_client = DockerClient::new(None); + match docker_client.get_volumes() { + Ok(volumes) => HttpResponse::Ok().json(volumes), + Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { + message: format!("Error getting docker volumes: {:?}", err), + }), + } +} + +// FIXME: The id is not being passed correctly +// I think the problem is in the enum DockerApi +#[get("/docker/volumes/{id}")] +async fn get_docker_volume(id: web::Path) -> impl Responder { + let mut docker_client = DockerClient::new(None); + + let cloned_id = id.clone(); + match docker_client.get_volume_details(cloned_id) { + Ok(volume) => HttpResponse::Ok().json(volume), + Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { + message: format!("Error getting docker volume {} details: {:?}", id, err), + }), + } +} diff --git a/crates/teus-docker/src/lib.rs b/crates/teus-docker/src/lib.rs new file mode 100644 index 0000000..c3d4495 --- /dev/null +++ b/crates/teus-docker/src/lib.rs @@ -0,0 +1 @@ +pub mod handlers; From 550467b5f49ff33f95b1caaac2f3b052c7137eca Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:03:59 +0200 Subject: [PATCH 13/21] Add system monitor crate with DB schema and queries The monitor crate provides functionality for collecting and storing system metrics like CPU usage, memory usage, and disk space. It includes database schema definitions, query functions, and a system info collector. --- crates/teus-monitor/Cargo.toml | 19 ++ crates/teus-monitor/src/lib.rs | 4 + crates/teus-monitor/src/mutation.rs | 53 +++ crates/teus-monitor/src/query.rs | 34 ++ crates/teus-monitor/src/schema.rs | 510 ++++++++++++++++++++++++++++ crates/teus-monitor/src/sys.rs | 410 ++++++++++++++++++++++ 6 files changed, 1030 insertions(+) create mode 100644 crates/teus-monitor/Cargo.toml create mode 100644 crates/teus-monitor/src/lib.rs create mode 100644 crates/teus-monitor/src/mutation.rs create mode 100644 crates/teus-monitor/src/query.rs create mode 100644 crates/teus-monitor/src/schema.rs create mode 100644 crates/teus-monitor/src/sys.rs diff --git a/crates/teus-monitor/Cargo.toml b/crates/teus-monitor/Cargo.toml new file mode 100644 index 0000000..04e113a --- /dev/null +++ b/crates/teus-monitor/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "teus-monitor" +version = "0.1.0" +edition = "2024" + +[dependencies] +teus-types = { path = "../teus-types" } +teus-database = { path = "../teus-database" } +teus-schema = { path = "../teus-schema"} +sysinfo = "0.33" +chrono = { version = "0.4", features = ["serde"] } +diesel = { version = "2.2.0", features = [ + "sqlite", + "returning_clauses_for_sqlite_3_35", +] } +serde = { version = "1.0.218", features = ["derive"] } + +[dev-dependencies] +serde_json = "1.0" diff --git a/crates/teus-monitor/src/lib.rs b/crates/teus-monitor/src/lib.rs new file mode 100644 index 0000000..9b0c93b --- /dev/null +++ b/crates/teus-monitor/src/lib.rs @@ -0,0 +1,4 @@ +pub mod mutation; +pub mod query; +pub mod schema; +pub mod sys; diff --git a/crates/teus-monitor/src/mutation.rs b/crates/teus-monitor/src/mutation.rs new file mode 100644 index 0000000..2f4bb5f --- /dev/null +++ b/crates/teus-monitor/src/mutation.rs @@ -0,0 +1,53 @@ +// src/monitor/mutation.rs +use crate::schema::{SchemaDiskInfo, SchemaSysInfo}; +use diesel::prelude::*; +use diesel::result::Error; + +/// Inserts system information into the database and returns the ID of the new record. +pub fn insert_sysinfo( + conn: &mut SqliteConnection, + new_sys_info: &SchemaSysInfo, +) -> Result { + use teus_schema::schema::sysinfo::dsl::*; + + diesel::insert_into(sysinfo) + .values(new_sys_info) + // SQLite doesn't directly support RETURNING id easily with diesel's insert helper + // So we insert, then query the last inserted row's id. + // This assumes single-threaded insertion or other mechanisms to prevent race conditions. + .execute(conn)?; + + // Retrieve the ID of the last inserted row for SQLite + let inserted_id = sysinfo + .select(id) + .order(id.desc()) + .first::>(conn)? + .ok_or(Error::NotFound)?; // Should exist if insert succeeded + + Ok(inserted_id) +} + +#[allow(dead_code)] +/// Inserts disk information into the database. +pub fn insert_diskinfo( + conn: &mut SqliteConnection, + new_disk_info: &SchemaDiskInfo, +) -> Result { + use teus_schema::schema::diskinfo::dsl::*; + + diesel::insert_into(diskinfo) + .values(new_disk_info) + .execute(conn) // Returns the number of affected rows +} + +/// Inserts multiple disk info entries efficiently. +pub fn insert_multiple_diskinfo( + conn: &mut SqliteConnection, + disk_infos: &[SchemaDiskInfo], +) -> Result { + use teus_schema::schema::diskinfo::dsl::*; + + diesel::insert_into(diskinfo) + .values(disk_infos) + .execute(conn) +} diff --git a/crates/teus-monitor/src/query.rs b/crates/teus-monitor/src/query.rs new file mode 100644 index 0000000..848a6dd --- /dev/null +++ b/crates/teus-monitor/src/query.rs @@ -0,0 +1,34 @@ +use crate::schema::{DiskInfo, SysInfo}; +use diesel::prelude::*; +use diesel::result::Error; + +/// Fetches the latest SysInfo record along with its associated DiskInfo records. +pub fn get_latest_sysinfo_with_disks( + conn: &mut SqliteConnection, +) -> Result)>, Error> { + use teus_schema::schema::sysinfo::dsl::*; + + let latest_sysinfo_option = sysinfo + .order(id.desc()) // Order by ID descending to get the latest + .select(SysInfo::as_select()) + .first::(conn) + .optional()?; + + match latest_sysinfo_option { + Some(latest_sysinfo) => { + /* import here because the ID is ambiguos */ + use teus_schema::schema::diskinfo::dsl::*; + + let disks = diskinfo + .filter(sysinfo_id.eq(latest_sysinfo.id.unwrap())) + .select(DiskInfo::as_select()) + .load::(conn)?; // Load all associated disks + + Ok(Some((latest_sysinfo, disks))) + } + None => { + // No SysInfo records found + Ok(None) + } + } +} diff --git a/crates/teus-monitor/src/schema.rs b/crates/teus-monitor/src/schema.rs new file mode 100644 index 0000000..2035fb3 --- /dev/null +++ b/crates/teus-monitor/src/schema.rs @@ -0,0 +1,510 @@ +//! Database schema structures for system monitoring data. +//! +//! This module defines the data structures used to store and retrieve +//! system monitoring information in the SQLite database. It includes +//! both insertable structures for writing new data and queryable +//! structures for reading existing data. + +use teus_schema::schema::{diskinfo, sysinfo}; +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +/// Structure for inserting system information records into the database. +/// +/// This structure represents a snapshot of system resource usage at a +/// specific point in time. It's designed to be inserted into the `sysinfo` +/// table and serves as the primary record for system monitoring data. +/// +/// # Database Schema +/// +/// Maps to the `sysinfo` table with the following constraints: +/// - `timestamp` should be in RFC3339 format for consistency +/// - All usage values are stored as floating-point numbers for precision +/// - Memory values are typically stored in bytes or megabytes +/// +/// # Examples +/// +/// ```rust +/// use teus::monitor::schema::SchemaSysInfo; +/// use chrono::Utc; +/// +/// let sys_info = SchemaSysInfo { +/// timestamp: Utc::now().to_rfc3339(), +/// cpu_usage: 25.5, +/// ram_usage: 4096.0, +/// total_ram: 16384.0, +/// free_ram: 8192.0, +/// used_swap: 512.0, +/// }; +/// ``` +#[derive(Insertable, Debug, Serialize, Deserialize)] +#[diesel(table_name = sysinfo)] +pub struct SchemaSysInfo { + /// Timestamp when this system information was collected. + /// + /// Should be in RFC3339 format (e.g., "2024-01-01T12:00:00Z") + /// for consistent parsing and sorting. + pub timestamp: String, + + /// CPU usage percentage at the time of collection. + /// + /// Range: 0.0 to 100.0, where 100.0 represents full CPU utilization. + pub cpu_usage: f32, + + /// Amount of RAM currently in use, in megabytes. + /// + /// This represents the memory actively being used by processes, + /// excluding cached and buffered memory. + pub ram_usage: f32, + + /// Total amount of RAM available in the system, in megabytes. + /// + /// This is the physical memory capacity and should remain + /// relatively constant unless hardware changes occur. + pub total_ram: f32, + + /// Amount of RAM currently free and available, in megabytes. + /// + /// This represents memory that is immediately available for + /// new processes without requiring swapping or cache eviction. + pub free_ram: f32, + + /// Amount of swap space currently in use, in megabytes. + /// + /// High swap usage may indicate memory pressure and can + /// significantly impact system performance. + pub used_swap: f32, +} + +/// Structure for inserting disk information records into the database. +/// +/// This structure represents disk usage information for a specific filesystem +/// at the time of system monitoring. Multiple disk records can be associated +/// with a single system information record through the `sysinfo_id` foreign key. +/// +/// # Database Relationships +/// +/// - `sysinfo_id`: Foreign key referencing the `sysinfo` table +/// - Each `SchemaSysInfo` record can have multiple associated `SchemaDiskInfo` records +/// +/// # Storage Units +/// +/// All size values are stored in megabytes for consistency and to avoid +/// integer overflow issues with very large storage devices. +/// +/// # Examples +/// +/// ```rust +/// use teus::monitor::schema::SchemaDiskInfo; +/// +/// let disk_info = SchemaDiskInfo { +/// sysinfo_id: 1, +/// filesystem: "ext4".to_string(), +/// size: 1000000, // 1TB in MB +/// used: 750000, // 750GB in MB +/// available: 250000, // 250GB in MB +/// used_percentage: 75, +/// mounted_path: "/".to_string(), +/// }; +/// ``` +#[derive(Insertable, Debug, Serialize, Deserialize)] +#[diesel(table_name = diskinfo)] +pub struct SchemaDiskInfo { + /// Foreign key reference to the associated system information record. + /// + /// This links the disk information to a specific monitoring snapshot, + /// allowing for historical tracking of disk usage over time. + pub sysinfo_id: i32, + + /// Type of filesystem (e.g., "ext4", "ntfs", "xfs", "btrfs"). + /// + /// This information helps identify the storage technology and + /// can be useful for performance analysis and troubleshooting. + pub filesystem: String, + + /// Total size of the filesystem in megabytes. + /// + /// This represents the total capacity of the storage device + /// or partition, including space used by the filesystem metadata. + pub size: i32, + + /// Amount of space currently used in megabytes. + /// + /// This includes all files, directories, and filesystem overhead, + /// but may not account for reserved space depending on the filesystem. + pub used: i32, + + /// Amount of space available for new data in megabytes. + /// + /// This is the space that can be immediately used for new files + /// and may be less than (total - used) due to filesystem reservations. + pub available: i32, + + /// Percentage of disk space currently in use. + /// + /// Range: 0 to 100, calculated as (used / total) * 100. + /// Values above 90% typically indicate the need for cleanup or expansion. + pub used_percentage: i32, + + /// Mount point or drive letter where the filesystem is accessible. + /// + /// Examples: "/", "/home", "/var", "C:", "D:" + /// This helps identify which part of the system's storage hierarchy + /// this disk information represents. + pub mounted_path: String, +} + +/// Structure for querying system information records from the database. +/// +/// This structure is used when retrieving system monitoring data from the +/// database. It includes the database-generated ID field and can be used +/// for displaying historical monitoring data, generating reports, and +/// API responses. +/// +/// # Usage Patterns +/// +/// - Retrieving recent system performance data for dashboards +/// - Historical analysis and trend reporting +/// - API endpoints that return monitoring data to clients +/// - Data export and backup operations +/// +/// # Examples +/// +/// ```rust +/// use teus::monitor::schema::SysInfo; +/// use diesel::prelude::*; +/// +/// // Query recent system information (pseudo-code) +/// // let recent_data: Vec = sysinfo::table +/// // .order(sysinfo::timestamp.desc()) +/// // .limit(10) +/// // .load(&mut connection)?; +/// ``` +#[derive(Queryable, Selectable, Identifiable, Debug, Serialize, Deserialize)] +#[diesel(table_name = sysinfo)] +pub struct SysInfo { + /// Database-generated unique identifier for this record. + /// + /// This is the primary key and is automatically assigned when + /// the record is inserted into the database. Used for referencing + /// this specific monitoring snapshot. + #[diesel(column_name = id)] + pub id: Option, + + /// Timestamp when this system information was collected. + /// + /// Stored in RFC3339 format for consistent parsing and timezone handling. + pub timestamp: String, + + /// CPU usage percentage at the time of collection. + /// + /// Range: 0.0 to 100.0, representing the overall CPU utilization + /// across all cores and threads. + pub cpu_usage: f32, + + /// Amount of RAM currently in use, in megabytes. + /// + /// Active memory usage excluding cached and buffered memory. + pub ram_usage: f32, + + /// Total amount of RAM available in the system, in megabytes. + /// + /// Physical memory capacity of the system. + pub total_ram: f32, + + /// Amount of RAM currently free and available, in megabytes. + /// + /// Memory immediately available for allocation to new processes. + pub free_ram: f32, + + /// Amount of swap space currently in use, in megabytes. + /// + /// High values may indicate memory pressure and performance issues. + pub used_swap: f32, +} + +/// Structure for querying disk information records from the database. +/// +/// This structure represents stored disk usage information that can be +/// retrieved for historical analysis, reporting, and API responses. +/// It includes the database-generated ID and maintains the relationship +/// to its parent system information record. +/// +/// # Relationships +/// +/// Each `DiskInfo` record is linked to a `SysInfo` record through +/// the `sysinfo_id` foreign key, allowing for comprehensive system +/// monitoring data retrieval. +/// +/// # Common Query Patterns +/// +/// - Retrieving disk usage trends over time +/// - Finding disks approaching capacity limits +/// - Generating storage utilization reports +/// - Monitoring filesystem-specific usage patterns +/// +/// # Examples +/// +/// ```rust +/// use teus::monitor::schema::DiskInfo; +/// use diesel::prelude::*; +/// +/// // Query disk info for high usage (pseudo-code) +/// // let high_usage_disks: Vec = diskinfo::table +/// // .filter(diskinfo::used_percentage.gt(90)) +/// // .load(&mut connection)?; +/// ``` +#[derive(Queryable, Selectable, Identifiable, Debug, Serialize, Deserialize)] +#[diesel(table_name = diskinfo)] +pub struct DiskInfo { + /// Database-generated unique identifier for this disk record. + /// + /// Primary key used for referencing this specific disk monitoring entry. + #[diesel(column_name = id)] + pub id: Option, + + /// Foreign key reference to the associated system information record. + /// + /// Links this disk information to a specific monitoring snapshot, + /// enabling time-series analysis of disk usage. + pub sysinfo_id: i32, + + /// Type of filesystem (e.g., "ext4", "ntfs", "xfs", "btrfs"). + /// + /// Identifies the storage technology and formatting of this disk. + pub filesystem: String, + + /// Total size of the filesystem in megabytes. + /// + /// Total capacity including filesystem overhead and reserved space. + pub size: i32, + + /// Amount of space currently used in megabytes. + /// + /// Space occupied by files, directories, and filesystem metadata. + pub used: i32, + + /// Amount of space available for new data in megabytes. + /// + /// Immediately usable space, may be less than (size - used) + /// due to filesystem reservations and overhead. + pub available: i32, + + /// Percentage of disk space currently in use. + /// + /// Range: 0 to 100, useful for quick assessment of storage pressure. + /// Values above 90% typically require attention. + pub used_percentage: i32, + + /// Mount point or drive letter where the filesystem is accessible. + /// + /// The path in the system's directory hierarchy where this + /// storage device can be accessed (e.g., "/", "/home", "C:"). + pub mounted_path: String, +} + +impl Default for SchemaSysInfo { + fn default() -> Self { + Self { + timestamp: "".to_string(), + cpu_usage: 0.0, + ram_usage: 0.0, + total_ram: 0.0, + free_ram: 0.0, + used_swap: 0.0, + // user_id: 0, + } + } +} + +impl Default for SchemaDiskInfo { + fn default() -> Self { + Self { + sysinfo_id: 0, + filesystem: "".to_string(), + size: 0, + used: 0, + available: 0, + used_percentage: 0, + mounted_path: "".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + + #[test] + fn test_schema_sys_info_default() { + let sys_info = SchemaSysInfo::default(); + assert_eq!(sys_info.timestamp, ""); + assert_eq!(sys_info.cpu_usage, 0.0); + assert_eq!(sys_info.ram_usage, 0.0); + assert_eq!(sys_info.total_ram, 0.0); + assert_eq!(sys_info.free_ram, 0.0); + assert_eq!(sys_info.used_swap, 0.0); + } + + #[test] + fn test_schema_disk_info_default() { + let disk_info = SchemaDiskInfo::default(); + assert_eq!(disk_info.sysinfo_id, 0); + assert_eq!(disk_info.filesystem, ""); + assert_eq!(disk_info.size, 0); + assert_eq!(disk_info.used, 0); + assert_eq!(disk_info.available, 0); + assert_eq!(disk_info.used_percentage, 0); + assert_eq!(disk_info.mounted_path, ""); + } + + #[test] + fn test_schema_sys_info_creation() { + let sys_info = SchemaSysInfo { + timestamp: Utc::now().to_rfc3339(), + cpu_usage: 25.5, + ram_usage: 1024.0, + total_ram: 8192.0, + free_ram: 4096.0, + used_swap: 512.0, + }; + + assert!(!sys_info.timestamp.is_empty()); + assert_eq!(sys_info.cpu_usage, 25.5); + assert_eq!(sys_info.ram_usage, 1024.0); + assert_eq!(sys_info.total_ram, 8192.0); + assert_eq!(sys_info.free_ram, 4096.0); + assert_eq!(sys_info.used_swap, 512.0); + } + + #[test] + fn test_schema_disk_info_creation() { + let disk_info = SchemaDiskInfo { + sysinfo_id: 1, + filesystem: "ext4".to_string(), + size: 1000, + used: 500, + available: 500, + used_percentage: 50, + mounted_path: "/".to_string(), + }; + + assert_eq!(disk_info.sysinfo_id, 1); + assert_eq!(disk_info.filesystem, "ext4"); + assert_eq!(disk_info.size, 1000); + assert_eq!(disk_info.used, 500); + assert_eq!(disk_info.available, 500); + assert_eq!(disk_info.used_percentage, 50); + assert_eq!(disk_info.mounted_path, "/"); + } + + #[test] + fn test_sys_info_serialization() { + let sys_info = SysInfo { + id: Some(1), + timestamp: Utc::now().to_rfc3339(), + cpu_usage: 25.5, + ram_usage: 1024.0, + total_ram: 8192.0, + free_ram: 4096.0, + used_swap: 512.0, + }; + + let serialized = serde_json::to_string(&sys_info).unwrap(); + assert!(serialized.contains("\"id\":1")); + assert!(serialized.contains("\"cpu_usage\":25.5")); + assert!(serialized.contains("\"ram_usage\":1024")); + } + + #[test] + fn test_disk_info_serialization() { + let disk_info = DiskInfo { + id: Some(1), + sysinfo_id: 1, + filesystem: "ext4".to_string(), + size: 1000, + used: 500, + available: 500, + used_percentage: 50, + mounted_path: "/".to_string(), + }; + + let serialized = serde_json::to_string(&disk_info).unwrap(); + assert!(serialized.contains("\"id\":1")); + assert!(serialized.contains("\"sysinfo_id\":1")); + assert!(serialized.contains("\"filesystem\":\"ext4\"")); + assert!(serialized.contains("\"mounted_path\":\"/\"")); + } + + #[test] + fn test_sys_info_debug_format() { + let sys_info = SysInfo { + id: Some(1), + timestamp: "2024-01-01T00:00:00Z".to_string(), + cpu_usage: 25.5, + ram_usage: 1024.0, + total_ram: 8192.0, + free_ram: 4096.0, + used_swap: 512.0, + }; + + let debug_str = format!("{:?}", sys_info); + assert!(debug_str.contains("SysInfo")); + assert!(debug_str.contains("25.5")); + assert!(debug_str.contains("1024")); + } + + #[test] + fn test_edge_values() { + // Test with extreme values + let sys_info = SchemaSysInfo { + timestamp: "2024-01-01T00:00:00Z".to_string(), + cpu_usage: 100.0, + ram_usage: 0.0, + total_ram: f32::MAX, + free_ram: f32::MAX, + used_swap: 0.0, + }; + + assert_eq!(sys_info.cpu_usage, 100.0); + assert_eq!(sys_info.ram_usage, 0.0); + assert_eq!(sys_info.total_ram, f32::MAX); + assert_eq!(sys_info.free_ram, f32::MAX); + assert_eq!(sys_info.used_swap, 0.0); + + // Test disk info with edge values + let disk_info = SchemaDiskInfo { + sysinfo_id: i32::MAX, + filesystem: "test".to_string(), + size: i32::MAX, + used: 0, + available: i32::MAX, + used_percentage: 100, + mounted_path: "/test".to_string(), + }; + + assert_eq!(disk_info.sysinfo_id, i32::MAX); + assert_eq!(disk_info.size, i32::MAX); + assert_eq!(disk_info.used, 0); + assert_eq!(disk_info.available, i32::MAX); + assert_eq!(disk_info.used_percentage, 100); + } + + #[test] + fn test_deserialization() { + let json_str = r#"{ + "timestamp": "2024-01-01T00:00:00Z", + "cpu_usage": 25.5, + "ram_usage": 1024.0, + "total_ram": 8192.0, + "free_ram": 4096.0, + "used_swap": 512.0 + }"#; + + let sys_info: SchemaSysInfo = serde_json::from_str(json_str).unwrap(); + assert_eq!(sys_info.timestamp, "2024-01-01T00:00:00Z"); + assert_eq!(sys_info.cpu_usage, 25.5); + assert_eq!(sys_info.ram_usage, 1024.0); + } +} diff --git a/crates/teus-monitor/src/sys.rs b/crates/teus-monitor/src/sys.rs new file mode 100644 index 0000000..c37909d --- /dev/null +++ b/crates/teus-monitor/src/sys.rs @@ -0,0 +1,410 @@ +use super::mutation; +use super::schema::{SchemaDiskInfo, SchemaSysInfo}; // Import the Diesel insertable structs +use teus_database::storage::Storage; +use teus_types::config::Config; +use chrono::Utc; +use teus_database::storage::TeuSQLiteConnection; +// use diesel::SqliteConnection; // Import SqliteConnection +use std::{thread, time::Duration}; // Import Mutex +use sysinfo::{Disks, MemoryRefreshKind, System}; + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct DiskInfo { + pub available: usize, + // Add disk-related fields here + pub filesystem: String, + pub mounted_path: String, + pub size: usize, + pub used: usize, + pub used_percentage: usize, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct SysInfo { + #[allow(dead_code)] + pub id: i64, + pub timestamp: String, + pub cpu_usage: f64, + pub ram_usage: f64, + pub total_ram: f64, + pub free_ram: f64, + pub used_swap: f64, + pub disks: Vec, +} + +#[allow(dead_code)] +impl SysInfo { + pub fn new( + cpu_usage: f64, + ram_usage: f64, + total_ram: f64, + free_ram: f64, + used_swap: f64, + disks: Vec, + ) -> Self { + Self { + id: 0, + timestamp: "".to_string(), + cpu_usage, + ram_usage, + total_ram, + free_ram, + used_swap, + disks, + } + } + + // Default constructor + pub fn default() -> Self { + Self { + id: 0, + timestamp: Utc::now().to_rfc3339(), + cpu_usage: 0.0, + ram_usage: 0.0, + total_ram: 0.0, + free_ram: 0.0, + used_swap: 0.0, + disks: vec![DiskInfo { + filesystem: String::new(), + size: 0, + used: 0, + available: 0, + used_percentage: 0, + mounted_path: String::new(), + }], + } + } + + pub fn run_monitor(mut self, config: &Config) { + let storage = match Storage::new(&config.database.path) { + Ok(storage) => storage, + Err(e) => { + eprintln!("Failed to create storage: {}", e); + return; + } + }; + + // Get a mutable connection from the Arc> + let mut conn_guard = match storage.diesel_conn.lock() { + Ok(guard) => guard, + Err(poisoned) => { + eprintln!("Failed to acquire lock on DB connection: {}", poisoned); + // Handle the poisoned mutex appropriately, maybe panic or return + return; + } + }; + // Dereference the guard to get the &mut SqliteConnection + let conn: &mut TeuSQLiteConnection = &mut *conn_guard; + + let mut sys = System::new_all(); + let disks_sysinfo = Disks::new_with_refreshed_list(); // Renamed to avoid conflict + + sys.refresh_all(); + sys.refresh_memory_specifics(MemoryRefreshKind::nothing().with_ram()); + + self.total_ram = sys.total_memory() as f64; + self.free_ram = sys.free_memory() as f64; + self.used_swap = sys.used_swap() as f64; + self.ram_usage = sys.used_memory() as f64; // Use used_memory for ram_usage + + thread::sleep(Duration::from_millis(250)); + sys.refresh_cpu_all(); + + let cpu_count = sys.cpus().len(); + let total_cpu_usage: f64 = sys.cpus().iter().map(|cpu| cpu.cpu_usage() as f64).sum(); + self.cpu_usage = if cpu_count > 0 { + total_cpu_usage / cpu_count as f64 + } else { + 0.0 + }; + + self.timestamp = Utc::now().to_rfc3339(); // Ensure timestamp is current + + // Create the SchemaSysInfo struct for insertion + let new_sys_info_to_insert = SchemaSysInfo { + timestamp: self.timestamp, + cpu_usage: self.cpu_usage as f32, // Cast f64 to f32 + ram_usage: self.ram_usage as f32, // Cast f64 to f32 + total_ram: self.total_ram as f32, // Cast f64 to f32 + free_ram: self.free_ram as f32, // Cast f64 to f32 + used_swap: self.used_swap as f32, // Cast f64 to f32 + }; + + // Insert system info using the SchemaSysInfo struct + let sysinfo_id = match mutation::insert_sysinfo(conn, &new_sys_info_to_insert) { + Ok(id) => id, + Err(e) => { + eprintln!("Failed to insert system info: {}", e); + // Drop the lock before returning + drop(conn_guard); + return; + } + }; + + // Prepare disk info data for batch insertion + let mut disk_infos_to_insert: Vec = Vec::new(); + for disk in disks_sysinfo.list() { + let space_used = disk.total_space() - disk.available_space(); + // Calculate usage percentage correctly + let usage_percentage = if disk.total_space() > 0 { + (space_used as f64 / disk.total_space() as f64 * 100.0) as i32 + } else { + 0 + }; + + let fs_name = disk.name().to_string_lossy().to_string(); + let mount_point = disk.mount_point().to_string_lossy().to_string(); + + disk_infos_to_insert.push(SchemaDiskInfo { + sysinfo_id, // Use the ID from the inserted sysinfo + filesystem: fs_name, + size: (disk.total_space() / 1024 / 1024) as i32, // Convert bytes to MB (adjust if needed) and cast usize to i32 + used: (space_used / 1024 / 1024) as i32, // Convert bytes to MB and cast usize to i32 + available: (disk.available_space() / 1024 / 1024) as i32, // Convert bytes to MB and cast usize to i32 + used_percentage: usage_percentage, // Use calculated percentage + mounted_path: mount_point, + }); + } + + // Insert disk info using the SchemaDiskInfo structs + if !disk_infos_to_insert.is_empty() { + if let Err(e) = mutation::insert_multiple_diskinfo(conn, &disk_infos_to_insert) { + eprintln!("Failed to insert disk info batch: {}", e); + } + } + // Lock is automatically dropped here when conn_guard goes out of scope + } +} + +#[cfg(test)] +mod tests { + use super::*; + use teus_types::config::{Config, DatabaseConfig, Environment, MonitorConfig, ServerConfig}; + + #[allow(dead_code)] + fn create_test_config() -> Config { + Config { + server: ServerConfig { + host: "127.0.0.1".to_string(), + port: 8080, + secret: "test_secret".to_string(), + environment: Environment::Test, + }, + database: DatabaseConfig { + path: ":memory:".to_string(), + }, + monitor: MonitorConfig { interval_secs: 60 }, + } + } + + #[test] + fn test_disk_info_creation() { + let disk_info = DiskInfo { + available: 1000, + filesystem: "ext4".to_string(), + mounted_path: "/".to_string(), + size: 2000, + used: 1000, + used_percentage: 50, + }; + + assert_eq!(disk_info.available, 1000); + assert_eq!(disk_info.filesystem, "ext4"); + assert_eq!(disk_info.mounted_path, "/"); + assert_eq!(disk_info.size, 2000); + assert_eq!(disk_info.used, 1000); + assert_eq!(disk_info.used_percentage, 50); + } + + #[test] + fn test_disk_info_clone() { + let disk_info = DiskInfo { + available: 500, + filesystem: "ntfs".to_string(), + mounted_path: "C:\\".to_string(), + size: 1000, + used: 500, + used_percentage: 50, + }; + + let cloned = disk_info.clone(); + assert_eq!(disk_info.available, cloned.available); + assert_eq!(disk_info.filesystem, cloned.filesystem); + assert_eq!(disk_info.mounted_path, cloned.mounted_path); + assert_eq!(disk_info.size, cloned.size); + assert_eq!(disk_info.used, cloned.used); + assert_eq!(disk_info.used_percentage, cloned.used_percentage); + } + + #[test] + fn test_sysinfo_new() { + let disks = vec![DiskInfo { + available: 1000, + filesystem: "ext4".to_string(), + mounted_path: "/".to_string(), + size: 2000, + used: 1000, + used_percentage: 50, + }]; + + let sysinfo = SysInfo::new(25.5, 8000.0, 16000.0, 8000.0, 2000.0, disks.clone()); + + assert_eq!(sysinfo.id, 0); + assert_eq!(sysinfo.timestamp, ""); + assert_eq!(sysinfo.cpu_usage, 25.5); + assert_eq!(sysinfo.ram_usage, 8000.0); + assert_eq!(sysinfo.total_ram, 16000.0); + assert_eq!(sysinfo.free_ram, 8000.0); + assert_eq!(sysinfo.used_swap, 2000.0); + assert_eq!(sysinfo.disks.len(), 1); + assert_eq!(sysinfo.disks[0].filesystem, "ext4"); + } + + #[test] + fn test_sysinfo_default() { + let sysinfo = SysInfo::default(); + + assert_eq!(sysinfo.id, 0); + assert!(!sysinfo.timestamp.is_empty()); // Should have a timestamp + assert_eq!(sysinfo.cpu_usage, 0.0); + assert_eq!(sysinfo.ram_usage, 0.0); + assert_eq!(sysinfo.total_ram, 0.0); + assert_eq!(sysinfo.free_ram, 0.0); + assert_eq!(sysinfo.used_swap, 0.0); + assert_eq!(sysinfo.disks.len(), 1); + assert_eq!(sysinfo.disks[0].filesystem, ""); + assert_eq!(sysinfo.disks[0].mounted_path, ""); + assert_eq!(sysinfo.disks[0].size, 0); + assert_eq!(sysinfo.disks[0].used, 0); + assert_eq!(sysinfo.disks[0].available, 0); + assert_eq!(sysinfo.disks[0].used_percentage, 0); + } + + #[test] + fn test_sysinfo_clone() { + let disks = vec![DiskInfo { + available: 2000, + filesystem: "btrfs".to_string(), + mounted_path: "/home".to_string(), + size: 4000, + used: 2000, + used_percentage: 50, + }]; + + let sysinfo = SysInfo::new(15.7, 4000.0, 8000.0, 4000.0, 1000.0, disks); + + let cloned = sysinfo.clone(); + assert_eq!(sysinfo.id, cloned.id); + assert_eq!(sysinfo.timestamp, cloned.timestamp); + assert_eq!(sysinfo.cpu_usage, cloned.cpu_usage); + assert_eq!(sysinfo.ram_usage, cloned.ram_usage); + assert_eq!(sysinfo.total_ram, cloned.total_ram); + assert_eq!(sysinfo.free_ram, cloned.free_ram); + assert_eq!(sysinfo.used_swap, cloned.used_swap); + assert_eq!(sysinfo.disks.len(), cloned.disks.len()); + assert_eq!(sysinfo.disks[0].filesystem, cloned.disks[0].filesystem); + } + + #[test] + fn test_sysinfo_debug_format() { + let sysinfo = SysInfo::default(); + let debug_str = format!("{:?}", sysinfo); + assert!(debug_str.contains("SysInfo")); + assert!(debug_str.contains("cpu_usage")); + assert!(debug_str.contains("ram_usage")); + } + + #[test] + fn test_disk_info_debug_format() { + let disk_info = DiskInfo { + available: 1000, + filesystem: "ext4".to_string(), + mounted_path: "/".to_string(), + size: 2000, + used: 1000, + used_percentage: 50, + }; + + let debug_str = format!("{:?}", disk_info); + assert!(debug_str.contains("DiskInfo")); + assert!(debug_str.contains("ext4")); + assert!(debug_str.contains("1000")); + } + + #[test] + fn test_sysinfo_edge_values() { + let disks = vec![]; + + // Test with extreme values + let sysinfo = SysInfo::new( + 100.0, // Max CPU usage + 0.0, // No RAM usage + u64::MAX as f64, // Maximum possible RAM + u64::MAX as f64, // Maximum free RAM + 0.0, // No swap usage + disks, + ); + + assert_eq!(sysinfo.cpu_usage, 100.0); + assert_eq!(sysinfo.ram_usage, 0.0); + assert_eq!(sysinfo.total_ram, u64::MAX as f64); + assert_eq!(sysinfo.free_ram, u64::MAX as f64); + assert_eq!(sysinfo.used_swap, 0.0); + assert_eq!(sysinfo.disks.len(), 0); + } + + #[test] + fn test_disk_info_percentage_calculation() { + // Test 100% usage + let full_disk = DiskInfo { + available: 0, + filesystem: "ext4".to_string(), + mounted_path: "/".to_string(), + size: 1000, + used: 1000, + used_percentage: 100, + }; + assert_eq!(full_disk.used_percentage, 100); + + // Test 0% usage + let empty_disk = DiskInfo { + available: 1000, + filesystem: "ext4".to_string(), + mounted_path: "/".to_string(), + size: 1000, + used: 0, + used_percentage: 0, + }; + assert_eq!(empty_disk.used_percentage, 0); + } + + #[test] + fn test_sysinfo_with_multiple_disks() { + let disks = vec![ + DiskInfo { + available: 1000, + filesystem: "ext4".to_string(), + mounted_path: "/".to_string(), + size: 2000, + used: 1000, + used_percentage: 50, + }, + DiskInfo { + available: 500, + filesystem: "ext4".to_string(), + mounted_path: "/home".to_string(), + size: 1000, + used: 500, + used_percentage: 50, + }, + ]; + + let sysinfo = SysInfo::new(30.0, 4000.0, 8000.0, 4000.0, 1000.0, disks); + + assert_eq!(sysinfo.disks.len(), 2); + assert_eq!(sysinfo.disks[0].mounted_path, "/"); + assert_eq!(sysinfo.disks[1].mounted_path, "/home"); + } +} From 6eaa7de4fc8bbdb781dd5a9aff36da9fd29be22c Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:04:24 +0200 Subject: [PATCH 14/21] Add Diesel schema for SQLite database tables --- crates/teus-schema/Cargo.toml | 7 ++++ crates/teus-schema/src/lib.rs | 1 + crates/teus-schema/src/schema.rs | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 crates/teus-schema/Cargo.toml create mode 100644 crates/teus-schema/src/lib.rs create mode 100644 crates/teus-schema/src/schema.rs diff --git a/crates/teus-schema/Cargo.toml b/crates/teus-schema/Cargo.toml new file mode 100644 index 0000000..ae39f30 --- /dev/null +++ b/crates/teus-schema/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "teus-schema" +version = "0.1.0" +edition = "2024" + +[dependencies] +diesel = { version = "2.2.0", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } diff --git a/crates/teus-schema/src/lib.rs b/crates/teus-schema/src/lib.rs new file mode 100644 index 0000000..1ce7e17 --- /dev/null +++ b/crates/teus-schema/src/lib.rs @@ -0,0 +1 @@ +pub mod schema; diff --git a/crates/teus-schema/src/schema.rs b/crates/teus-schema/src/schema.rs new file mode 100644 index 0000000..d2737c9 --- /dev/null +++ b/crates/teus-schema/src/schema.rs @@ -0,0 +1,57 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + config (id) { + id -> Nullable, + first_visit -> Bool, + } +} + +diesel::table! { + diskinfo (id) { + id -> Nullable, + sysinfo_id -> Integer, + filesystem -> Text, + size -> Integer, + used -> Integer, + available -> Integer, + used_percentage -> Integer, + mounted_path -> Text, + } +} + +diesel::table! { + services (id) { + id -> Nullable, + name -> Text, + link -> Text, + icon -> Nullable, + user_id -> Integer, + } +} + +diesel::table! { + sysinfo (id) { + id -> Nullable, + timestamp -> Text, + cpu_usage -> Float, + ram_usage -> Float, + total_ram -> Float, + free_ram -> Float, + used_swap -> Float, + } +} + +diesel::table! { + user (id) { + id -> Nullable, + username -> Text, + password -> Text, + salt -> Text, + } +} + +diesel::joinable!(diskinfo -> sysinfo (sysinfo_id)); +diesel::joinable!(services -> user (user_id)); + +diesel::allow_tables_to_appear_in_same_query!(config, diskinfo, services, sysinfo, user,); From 540b75a609a6d3fae18c731fdf20fd40d0080f56 Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:05:00 +0200 Subject: [PATCH 15/21] Add bookmarks service crate with CRUD operations --- crates/teus-services/Cargo.toml | 18 +++ .../teus-services/src/bookmarks/handlers.rs | 152 ++++++++++++++++++ crates/teus-services/src/bookmarks/mod.rs | 3 + .../teus-services/src/bookmarks/mutation.rs | 95 +++++++++++ crates/teus-services/src/bookmarks/schema.rs | 52 ++++++ crates/teus-services/src/lib.rs | 1 + 6 files changed, 321 insertions(+) create mode 100644 crates/teus-services/Cargo.toml create mode 100644 crates/teus-services/src/bookmarks/handlers.rs create mode 100644 crates/teus-services/src/bookmarks/mod.rs create mode 100644 crates/teus-services/src/bookmarks/mutation.rs create mode 100644 crates/teus-services/src/bookmarks/schema.rs create mode 100644 crates/teus-services/src/lib.rs diff --git a/crates/teus-services/Cargo.toml b/crates/teus-services/Cargo.toml new file mode 100644 index 0000000..151d328 --- /dev/null +++ b/crates/teus-services/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "teus-services" +version = "0.1.0" +edition = "2024" + +[dependencies] +teus-monitor = { path = "../teus-monitor" } +teus-types = { path = "../teus-types" } +teus-auth = { path = "../teus-auth" } +teus-database = { path = "../teus-database" } +teus-schema = { path = "../teus-schema" } +serde_json = "1.0" +actix-web = "4.9.0" +serde = { version = "1.0.218", features = ["derive"] } +diesel = { version = "2.2.0", features = [ + "sqlite", + "returning_clauses_for_sqlite_3_35", +] } diff --git a/crates/teus-services/src/bookmarks/handlers.rs b/crates/teus-services/src/bookmarks/handlers.rs new file mode 100644 index 0000000..7562787 --- /dev/null +++ b/crates/teus-services/src/bookmarks/handlers.rs @@ -0,0 +1,152 @@ +use crate::bookmarks::schema::{NewService, Service, ServicePatchPayload, ServicePayload}; +use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder, delete, get, patch, post, web}; +use teus_auth::middleware::Claims; +use teus_database::storage::Storage; +use teus_types::config::Config; + +#[allow(dead_code)] +/// Helper function to extract claims from request +fn extract_claims_from_request(req: &HttpRequest) -> Result { + req.extensions().get::().cloned().ok_or_else(|| { + HttpResponse::Unauthorized().json(serde_json::json!({ + "error": "No authentication claims found" + })) + }) +} + +#[get("/bookmarks")] +/// Get all services for the authenticated user +pub async fn get_user_services( + req: HttpRequest, + config: actix_web::web::Data, +) -> impl Responder { + // Clone the claims to own them + let claims = match req.extensions().get::().cloned() { + Some(claims) => claims, + None => { + return HttpResponse::Unauthorized().json(serde_json::json!({ + "error": "No authentication claims found" + })); + } + }; + + let user_id = claims.id; + let storage = Storage::new(&config.database.path).unwrap(); + let mut conn = storage.diesel_conn.lock().unwrap(); + let services = + Service::get_services_by_user_id(&mut conn, user_id).expect("Error getting services"); + + HttpResponse::Ok().json(serde_json::json!(services)) +} + +#[post("/bookmarks")] +/// Add a new service for the authenticated user +pub async fn add_service( + req: HttpRequest, + service_data: web::Json, + config: actix_web::web::Data, +) -> impl Responder { + // if !service_data.values() {} + let claims = extract_claims_from_request(&req).expect("Cannot extract claims from request"); + let new_service = NewService { + name: service_data.name.clone(), + link: service_data.link.clone(), + icon: service_data.icon.clone(), + user_id: claims.id, + }; + + let storage = Storage::new(&config.database.path).unwrap(); + let mut conn = storage.diesel_conn.lock().unwrap(); + + let service_added = match Service::add_service(&mut conn, new_service) { + Ok(service) => service, + Err(_) => { + return HttpResponse::InternalServerError().json(serde_json::json!({ + "message": "Error creating a new Service", + })); + } + }; + + HttpResponse::Created().json(service_added) +} + +#[delete("/bookmarks/{id}")] +pub async fn delete_service_by_id( + id: web::Path, + req: HttpRequest, + config: actix_web::web::Data, +) -> impl Responder { + let claims = extract_claims_from_request(&req).expect("Cannot extract claims from request"); + let user_id = claims.id; + let bookmark_id = id.clone(); + + let storage = Storage::new(&config.database.path).unwrap(); + let mut conn = storage.diesel_conn.lock().unwrap(); + + match Service::_get_service_by_id(&mut conn, bookmark_id) { + Ok(service) => { + if service.user_id != user_id { + return HttpResponse::Unauthorized().json(serde_json::json!({ + "message": "You are not authorized to delete this service" + })); + } + + match Service::delete_service(&mut conn, bookmark_id, user_id) { + Ok(rows_affected) => { + if rows_affected > 0 { + HttpResponse::NoContent().finish() + } else { + HttpResponse::InternalServerError().json(serde_json::json!({ + "message": "Unexpected error during deletion" + })) + } + } + Err(_) => HttpResponse::InternalServerError().json(serde_json::json!({ + "message": "Error deleting bookmark" + })), + } + } + Err(_) => { + // Service doesn't exist + HttpResponse::NotFound().json(serde_json::json!({ + "message": "Service not found" + })) + } + } +} + +#[patch("/bookmarks/{id}")] +pub async fn update_service_by_id( + id: web::Path, + service_data: web::Json, + req: HttpRequest, + config: actix_web::web::Data, +) -> impl Responder { + let claims = extract_claims_from_request(&req).expect("Cannot extract claims from request"); + let user_id = claims.id; + let bookmark_id = id.clone(); + + let storage = Storage::new(&config.database.path).unwrap(); + let mut conn = storage.diesel_conn.lock().unwrap(); + + match Service::_get_service_by_id(&mut conn, bookmark_id) { + Ok(service) => { + if service.user_id != user_id { + return HttpResponse::Unauthorized().json(serde_json::json!({ + "message": "You are not authorized to update this service" + })); + } + + match Service::patch_service(&mut conn, bookmark_id, user_id, service_data.into_inner()) + { + Ok(service) => HttpResponse::Ok().json(service), + Err(_) => HttpResponse::InternalServerError().json(serde_json::json!({ + "message": "Error updating bookmark" + })), + } + } + Err(_) => HttpResponse::NotFound().json(serde_json::json!({ + "message": "Service not found" + })), + } +} diff --git a/crates/teus-services/src/bookmarks/mod.rs b/crates/teus-services/src/bookmarks/mod.rs new file mode 100644 index 0000000..fc95d25 --- /dev/null +++ b/crates/teus-services/src/bookmarks/mod.rs @@ -0,0 +1,3 @@ +pub mod handlers; +pub mod mutation; +pub mod schema; diff --git a/crates/teus-services/src/bookmarks/mutation.rs b/crates/teus-services/src/bookmarks/mutation.rs new file mode 100644 index 0000000..1a1cd28 --- /dev/null +++ b/crates/teus-services/src/bookmarks/mutation.rs @@ -0,0 +1,95 @@ +use super::schema::{NewService, Service, ServicePatchPayload}; +use diesel::prelude::*; +use diesel::result::Error; +use diesel::{RunQueryDsl, SqliteConnection}; + +impl Service { + /// Add a new service to the database + pub fn add_service( + conn: &mut SqliteConnection, + new_service: NewService, + ) -> Result { + use teus_schema::schema::services; + + diesel::insert_into(services::table) + .values(&new_service) + .returning(Service::as_returning()) + .get_result(conn) + } + + /// Get all services for a specific user + pub fn get_services_by_user_id( + conn: &mut SqliteConnection, + user_id_claim: i32, + ) -> Result, Error> { + use teus_schema::schema::services::dsl::*; + + services + .filter(user_id.eq(user_id_claim)) + .select(Service::as_select()) + .load(conn) + } + + /// Get a specific service by ID + pub fn _get_service_by_id( + conn: &mut SqliteConnection, + service_id: i32, + ) -> Result { + use teus_schema::schema::services::dsl::*; + + services + .filter(id.eq(service_id)) + .select(Service::as_select()) + .first(conn) + } + + /// Update a service + pub fn update_service( + conn: &mut SqliteConnection, + service_id: i32, + user_query_id: i32, + updated_service: NewService, + ) -> Result { + use teus_schema::schema::services::dsl::*; + + diesel::update(services.filter(id.eq(service_id).and(user_id.eq(user_query_id)))) + .set(( + name.eq(&updated_service.name), + link.eq(&updated_service.link), + icon.eq(&updated_service.icon), + user_id.eq(&updated_service.user_id), + )) + .returning(Service::as_returning()) + .get_result(conn) + } + + /// Update a service with partial data (PATCH operation) + pub fn patch_service( + conn: &mut SqliteConnection, + service_id: i32, + user_query_id: i32, + patch_data: ServicePatchPayload, + ) -> Result { + let current_service = Self::_get_service_by_id(conn, service_id)?; + let updated_service = NewService { + name: patch_data.name.unwrap_or(current_service.name), + link: patch_data.link.unwrap_or(current_service.link), + icon: patch_data.icon.or(current_service.icon), + user_id: user_query_id, + }; + + Self::update_service(conn, service_id, user_query_id, updated_service) + } + + /// Delete a service + pub fn delete_service( + conn: &mut SqliteConnection, + service_id: i32, + user_query_id: i32, + ) -> Result { + use teus_schema::schema::services::dsl::*; + + diesel::delete(services.filter(id.eq(service_id).and(user_id.eq(user_query_id)))) + .execute(conn) + } +} diff --git a/crates/teus-services/src/bookmarks/schema.rs b/crates/teus-services/src/bookmarks/schema.rs new file mode 100644 index 0000000..f4f96eb --- /dev/null +++ b/crates/teus-services/src/bookmarks/schema.rs @@ -0,0 +1,52 @@ +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +/* schemas */ +use teus_schema::schema; + +#[allow(dead_code)] +pub type Bookmarks = Vec; + +// For querying existing services from the database +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Queryable, Selectable)] +#[diesel(table_name = schema::services)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +#[serde(rename_all = "camelCase")] +pub struct Service { + pub id: Option, + pub name: String, + pub link: String, + pub icon: Option, + pub user_id: i32, +} + +// For inserting new services into the database +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Insertable)] +#[diesel(table_name = schema::services)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +#[serde(rename_all = "camelCase")] +pub struct NewService { + pub name: String, + pub link: String, + pub icon: Option, + pub user_id: i32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NewServiceSchema { + pub name: String, + pub link: String, + pub icon: Option, +} + +// For backwards compatibility if you still need BookmarkService +pub type BookmarkService = Service; +pub type ServicePayload = NewServiceSchema; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ServicePatchPayload { + pub name: Option, + pub link: Option, + pub icon: Option, +} diff --git a/crates/teus-services/src/lib.rs b/crates/teus-services/src/lib.rs new file mode 100644 index 0000000..bf3e888 --- /dev/null +++ b/crates/teus-services/src/lib.rs @@ -0,0 +1 @@ +pub mod bookmarks; From d8155000fcc621a54ff1038649515b38dd544bfd Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:05:11 +0200 Subject: [PATCH 16/21] Add initial Diesel ORM configuration file --- diesel.toml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 diesel.toml diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..c1421d8 --- /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/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "/Users/homeerr/Documents/code/rust/teus/migrations" From 8accf308e0dbcf4c6d81a478a10fab23fd39d49a Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:09:35 +0200 Subject: [PATCH 17/21] Cargo fmt --- crates/teus-api/src/handlers/systeminfo.rs | 4 ++-- crates/teus-api/src/lib.rs | 2 +- crates/teus-auth/src/handlers.rs | 12 ++++++------ crates/teus-auth/src/middleware.rs | 8 ++++---- crates/teus-auth/src/mutation.rs | 2 +- crates/teus-config/src/config/parser.rs | 2 +- crates/teus-docker/src/handlers.rs | 2 +- crates/teus-monitor/src/schema.rs | 2 +- crates/teus-monitor/src/sys.rs | 4 ++-- teus/main.rs | 8 ++++---- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/teus-api/src/handlers/systeminfo.rs b/crates/teus-api/src/handlers/systeminfo.rs index c659c91..303f165 100644 --- a/crates/teus-api/src/handlers/systeminfo.rs +++ b/crates/teus-api/src/handlers/systeminfo.rs @@ -1,6 +1,6 @@ -use teus_types::api_models::{GenericSysInfoResponse, IpInfo, MACInfo}; -use actix_web::{get, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, get}; use sysinfo::{Networks, System}; +use teus_types::api_models::{GenericSysInfoResponse, IpInfo, MACInfo}; /* TODO: Migrate those services into teus-services crate */ fn collect_network_info() -> Vec { diff --git a/crates/teus-api/src/lib.rs b/crates/teus-api/src/lib.rs index dd38790..c0c696a 100644 --- a/crates/teus-api/src/lib.rs +++ b/crates/teus-api/src/lib.rs @@ -1,2 +1,2 @@ -pub mod routes; pub mod handlers; +pub mod routes; diff --git a/crates/teus-auth/src/handlers.rs b/crates/teus-auth/src/handlers.rs index 404cd57..9ddab25 100644 --- a/crates/teus-auth/src/handlers.rs +++ b/crates/teus-auth/src/handlers.rs @@ -1,16 +1,16 @@ -use teus_database::storage::Storage; use crate::middleware::Claims; use crate::schema::User; -use teus_config::config::schema::TeusConfig; -use teus_types::config::Config; -use actix_web::{post, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, post, web}; use argon2::{ - password_hash::{PasswordHash, PasswordVerifier}, Argon2, + password_hash::{PasswordHash, PasswordVerifier}, }; use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header}; +use jsonwebtoken::{EncodingKey, Header, encode}; use serde::{Deserialize, Serialize}; +use teus_config::config::schema::TeusConfig; +use teus_database::storage::Storage; +use teus_types::config::Config; /// Request structure for user authentication login endpoint. /// diff --git a/crates/teus-auth/src/middleware.rs b/crates/teus-auth/src/middleware.rs index e0e46f0..2c15473 100644 --- a/crates/teus-auth/src/middleware.rs +++ b/crates/teus-auth/src/middleware.rs @@ -5,14 +5,14 @@ //! from the Authorization header, making user claims available to handlers. use actix_web::{ - dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, - error::ErrorUnauthorized, Error, HttpMessage, + dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready}, + error::ErrorUnauthorized, }; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; use serde::{Deserialize, Serialize}; use std::{ - future::{ready, Future, Ready}, + future::{Future, Ready, ready}, pin::Pin, rc::Rc, }; diff --git a/crates/teus-auth/src/mutation.rs b/crates/teus-auth/src/mutation.rs index 81156e9..39e50f0 100644 --- a/crates/teus-auth/src/mutation.rs +++ b/crates/teus-auth/src/mutation.rs @@ -1,6 +1,6 @@ use super::schema::User; -use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::SaltString; +use argon2::password_hash::rand_core::OsRng; use argon2::{Argon2, PasswordHasher}; use diesel::result::Error; use diesel::{RunQueryDsl, SqliteConnection}; diff --git a/crates/teus-config/src/config/parser.rs b/crates/teus-config/src/config/parser.rs index db8ec77..4e1b46b 100644 --- a/crates/teus-config/src/config/parser.rs +++ b/crates/teus-config/src/config/parser.rs @@ -1,6 +1,6 @@ -use teus_types::config::Config; use std::error::Error; use std::{fs, path::Path}; +use teus_types::config::Config; #[allow(dead_code)] type GeneralError = Box; diff --git a/crates/teus-docker/src/handlers.rs b/crates/teus-docker/src/handlers.rs index 0e802e8..f9e0fa7 100644 --- a/crates/teus-docker/src/handlers.rs +++ b/crates/teus-docker/src/handlers.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use actix_web::{get, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, get, web}; use docker::docker::DockerClient; use serde::{Deserialize, Serialize}; use serde_qs::to_string; diff --git a/crates/teus-monitor/src/schema.rs b/crates/teus-monitor/src/schema.rs index 2035fb3..e64d7e1 100644 --- a/crates/teus-monitor/src/schema.rs +++ b/crates/teus-monitor/src/schema.rs @@ -5,9 +5,9 @@ //! both insertable structures for writing new data and queryable //! structures for reading existing data. -use teus_schema::schema::{diskinfo, sysinfo}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; +use teus_schema::schema::{diskinfo, sysinfo}; /// Structure for inserting system information records into the database. /// diff --git a/crates/teus-monitor/src/sys.rs b/crates/teus-monitor/src/sys.rs index c37909d..475990a 100644 --- a/crates/teus-monitor/src/sys.rs +++ b/crates/teus-monitor/src/sys.rs @@ -1,9 +1,9 @@ use super::mutation; use super::schema::{SchemaDiskInfo, SchemaSysInfo}; // Import the Diesel insertable structs -use teus_database::storage::Storage; -use teus_types::config::Config; use chrono::Utc; +use teus_database::storage::Storage; use teus_database::storage::TeuSQLiteConnection; +use teus_types::config::Config; // use diesel::SqliteConnection; // Import SqliteConnection use std::{thread, time::Duration}; // Import Mutex use sysinfo::{Disks, MemoryRefreshKind, System}; diff --git a/teus/main.rs b/teus/main.rs index b48766d..78a15a0 100644 --- a/teus/main.rs +++ b/teus/main.rs @@ -1,7 +1,3 @@ -use teus_config::config; -use teus_database::storage; -use teus_api::routes; -use teus_monitor::sys::SysInfo; use std::{ env, path::Path, @@ -12,6 +8,10 @@ use std::{ }, thread, }; +use teus_api::routes; +use teus_config::config; +use teus_database::storage; +use teus_monitor::sys::SysInfo; fn main() { println!("Starting Teus service..."); From 99a208cab8ce24e72d8578b033612b7d75cde41c Mon Sep 17 00:00:00 2001 From: imggion Date: Mon, 7 Jul 2025 02:15:35 +0200 Subject: [PATCH 18/21] Update workspace dependencies and clean up project --- Cargo.lock | 85 +++++++++----------------------------------------- Cargo.toml | 63 +++++++++++++++++-------------------- teus/lib.rs | 1 - teus/schema.rs | 57 --------------------------------- 4 files changed, 43 insertions(+), 163 deletions(-) delete mode 100644 teus/schema.rs diff --git a/Cargo.lock b/Cargo.lock index 7b20fc9..fbf2246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,9 +538,9 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.5" +version = "3.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" dependencies = [ "nix", "windows-sys 0.59.0", @@ -689,12 +689,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "dsl_auto_type" version = "0.1.3" @@ -740,18 +734,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fastrand" version = "2.3.0" @@ -774,12 +756,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" - [[package]] name = "foreign-types" version = "0.3.2" @@ -928,18 +904,6 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown", -] [[package]] name = "heck" @@ -1313,9 +1277,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libsqlite3-sys" @@ -1424,9 +1388,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", @@ -1801,20 +1765,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rusqlite" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2117,6 +2067,10 @@ dependencies = [ "syn", ] +[[package]] +name = "sysd" +version = "0.1.0" + [[package]] name = "sysinfo" version = "0.33.1" @@ -2169,29 +2123,18 @@ dependencies = [ name = "teus" version = "0.1.0" dependencies = [ - "actix-cors", - "actix-web", - "argon2", - "chrono", "ctrlc", - "derive_more 2.0.1", - "diesel", - "docker", - "dotenvy", - "jsonwebtoken", - "rusqlite", - "serde", - "serde_json", - "serde_qs", - "sysinfo", "tempfile", "teus-api", + "teus-auth", "teus-config", "teus-database", + "teus-docker", "teus-monitor", + "teus-services", "teus-types", + "tokio", "tokio-test", - "toml", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9e46181..bc27ee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,44 @@ [workspace] -# This file is part of the Teus project. members = [ + ".", "crates/teus-types", - "crates/docker", "crates/teus-config", - "crates/teus-schema", "crates/teus-database", "crates/teus-monitor", "crates/teus-auth", - "crates/teus-docker", "crates/teus-api", "crates/teus-services", + "crates/teus-docker", + "crates/sysd", ] default-members = ["."] +[workspace.dependencies] +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +chrono = { version = "0.4", features = ["serde"] } +diesel = { version = "2.2.0", features = [ + "sqlite", + "returning_clauses_for_sqlite_3_35", +] } +actix-web = "4.9" + +[dependencies] +teus-types = { path = "crates/teus-types" } +teus-config = { path = "crates/teus-config" } +teus-database = { path = "crates/teus-database" } +teus-monitor = { path = "crates/teus-monitor" } +teus-auth = { path = "crates/teus-auth" } +teus-api = { path = "crates/teus-api" } +teus-services = { path = "crates/teus-services" } +teus-docker = { path = "crates/teus-docker" } +tokio = { version = "1", features = ["full"] } +ctrlc = "3.4" + +[dev-dependencies] +tempfile = "3.8" +tokio-test = "0.4" + [package] name = "teus" version = "0.1.0" @@ -28,36 +53,6 @@ path = "teus/lib.rs" name = "teus" path = "./teus/main.rs" -[dependencies] # we need to clean something here -docker = { path = "crates/docker" } -teus-types = { path = "crates/teus-types" } -teus-config = { path = "crates/teus-config" } -teus-database = { path = "crates/teus-database" } -teus-api = { path = "crates/teus-api" } -teus-monitor = { path = "crates/teus-monitor" } -diesel = { version = "2.2.0", features = [ - "sqlite", - "returning_clauses_for_sqlite_3_35", -] } -dotenvy = "0.15" -actix-cors = "0.7.0" -actix-web = "4.9.0" -chrono = "0.4.40" -ctrlc = "3.4.5" -derive_more = { version = "2.0.1", features = ["display", "error", "from"] } -jsonwebtoken = "9.3.1" -rusqlite = "0.34.0" -serde = { version = "1.0.218", features = ["derive"] } -serde_json = "1.0" -serde_qs = "0.13" -sysinfo = "0.33.1" -toml = "0.8.20" -argon2 = "0.5.3" - -[dev-dependencies] -tempfile = "3.8" -tokio-test = "0.4" - [profile.release] opt-level = "z" # Optimize for size lto = true # Enable link-time optimization diff --git a/teus/lib.rs b/teus/lib.rs index 7ab37a3..b5614dd 100644 --- a/teus/lib.rs +++ b/teus/lib.rs @@ -1,2 +1 @@ -pub mod schema; pub mod utils; diff --git a/teus/schema.rs b/teus/schema.rs deleted file mode 100644 index d2737c9..0000000 --- a/teus/schema.rs +++ /dev/null @@ -1,57 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - config (id) { - id -> Nullable, - first_visit -> Bool, - } -} - -diesel::table! { - diskinfo (id) { - id -> Nullable, - sysinfo_id -> Integer, - filesystem -> Text, - size -> Integer, - used -> Integer, - available -> Integer, - used_percentage -> Integer, - mounted_path -> Text, - } -} - -diesel::table! { - services (id) { - id -> Nullable, - name -> Text, - link -> Text, - icon -> Nullable, - user_id -> Integer, - } -} - -diesel::table! { - sysinfo (id) { - id -> Nullable, - timestamp -> Text, - cpu_usage -> Float, - ram_usage -> Float, - total_ram -> Float, - free_ram -> Float, - used_swap -> Float, - } -} - -diesel::table! { - user (id) { - id -> Nullable, - username -> Text, - password -> Text, - salt -> Text, - } -} - -diesel::joinable!(diskinfo -> sysinfo (sysinfo_id)); -diesel::joinable!(services -> user (user_id)); - -diesel::allow_tables_to_appear_in_same_query!(config, diskinfo, services, sysinfo, user,); From 842ff07b05a51a0687e9b2444d502ba2672af255 Mon Sep 17 00:00:00 2001 From: Giovanni D'Andrea Date: Wed, 9 Jul 2025 23:42:00 +0200 Subject: [PATCH 19/21] Refactor DockerClient initialization to return Result for error handling, implement custom Clone for DockerClient, and integrate DockerClient into Actix web service with shared state management. Add ApiError for handling Docker-related errors in API responses. --- crates/docker/src/docker.rs | 33 +++++++--- crates/teus-api/Cargo.toml | 1 + crates/teus-api/src/routes.rs | 10 +++ crates/teus-docker/src/errors.rs | 31 ++++++++++ crates/teus-docker/src/handlers.rs | 97 ++++++++++++++++++------------ crates/teus-docker/src/lib.rs | 3 +- 6 files changed, 125 insertions(+), 50 deletions(-) create mode 100644 crates/teus-docker/src/errors.rs diff --git a/crates/docker/src/docker.rs b/crates/docker/src/docker.rs index 42ce0ef..9ff7195 100644 --- a/crates/docker/src/docker.rs +++ b/crates/docker/src/docker.rs @@ -1257,15 +1257,17 @@ impl DockerClient { /// /// If `socket_path` is `None`, it defaults to the standard Unix socket path /// "/var/run/docker.sock". - pub fn new(socket_path: Option) -> Self { + pub fn new(socket_path: Option) -> Result { // If socket_path is Some(path), use it. // If socket_path is None, execute the closure to get the default path. let path = socket_path.unwrap_or_else(|| DOCKER_SOCK.to_string()); - DockerClient { - // We now pass the guaranteed-to-be-valid path to the builder - request_builder: TeusRequestBuilder::new(path, "localhost".to_string()) - .expect("Are you sure docker is up and running?"), + match TeusRequestBuilder::new(path, "localhost".to_string()) { + Ok(request_builder) => { + let client = DockerClient { request_builder }; + Ok(client) + } + Err(_) => Err(DockerError::Generic("Error accessing Docker".to_string())), } } @@ -1337,6 +1339,19 @@ impl DockerClient { } } +/* this custom clone is needed because the docker client contains an UnixStream that is unique for safety purposes */ +impl Clone for DockerClient { + fn clone(&self) -> Self { + let socket_path = self.request_builder.socket.clone(); + let host = self.request_builder.host.clone(); + + match TeusRequestBuilder::new(socket_path, host) { + Ok(request_builder) => DockerClient { request_builder }, + Err(_) => panic!("Failed to clone DockerClient: could not establish new connection"), + } + } +} + mod tests { #[allow(unused_imports)] use super::*; @@ -1364,7 +1379,7 @@ mod tests { fn test_get_containers() { // Our test now calls the correct helper function automatically. let test_socket = get_test_socket_path(); - let mut client = DockerClient::new(test_socket); + let mut client = DockerClient::new(test_socket).unwrap(); println!("{:?}", client); let containers = client.get_containers(None).unwrap(); @@ -1376,7 +1391,7 @@ mod tests { fn test_get_version() { // Our test now calls the correct helper function automatically. let test_socket = get_test_socket_path(); - let mut client = DockerClient::new(test_socket); + let mut client = DockerClient::new(test_socket).unwrap(); println!("{:?}", client); let version = client.get_version().unwrap(); @@ -1388,7 +1403,7 @@ mod tests { fn test_get_volumes() { // Our test now calls the correct helper function automatically. let test_socket = get_test_socket_path(); - let mut client = DockerClient::new(test_socket); + let mut client = DockerClient::new(test_socket).unwrap(); println!("{:?}", client); let volumes = client.get_volumes().unwrap(); @@ -1401,7 +1416,7 @@ mod tests { fn test_get_volume_details() { // Our test now calls the correct helper function automatically. let test_socket = get_test_socket_path(); - let mut client = DockerClient::new(test_socket); + let mut client = DockerClient::new(test_socket).unwrap(); println!("{:?}", client); let volume_name = diff --git a/crates/teus-api/Cargo.toml b/crates/teus-api/Cargo.toml index 57f59e5..2a02042 100644 --- a/crates/teus-api/Cargo.toml +++ b/crates/teus-api/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +docker = { path = "../docker" } teus-types = { path = "../teus-types" } teus-auth = { path = "../teus-auth" } teus-monitor = { path = "../teus-monitor" } diff --git a/crates/teus-api/src/routes.rs b/crates/teus-api/src/routes.rs index a5d1be2..46508ab 100644 --- a/crates/teus-api/src/routes.rs +++ b/crates/teus-api/src/routes.rs @@ -1,10 +1,13 @@ /* REMOVE SERVICES FROM THIS CRATE */ /* THIS CRATE IS ONLY FOR HANDLE THE ACTIX WEBSERVICE */ +use std::sync::Mutex; + use crate::handlers::systeminfo; use actix_cors::Cors; use actix_web::error::ErrorInternalServerError; use actix_web::{App, Error, HttpResponse, HttpServer, get, http, middleware, web}; +use docker::docker::DockerClient; use teus_auth::handlers::{JwtConfig, check, login, signup}; use teus_auth::middleware::AuthMiddlewareFactory; use teus_config::config::handlers::get_teus_config; @@ -18,6 +21,9 @@ use teus_services::bookmarks::handlers as bookmark_handlers; use teus_types::api_models::{DiskInfoResponse, SysInfoResponse}; use teus_types::config::Config; +pub type DockerState = web::Data>>; + + // TODO: move this api into another file `syshandler` or something #[get("/sysinfo")] async fn sysinfo_handler(storage: web::Data) -> Result { @@ -68,6 +74,9 @@ pub async fn start_webserver(config: &Config, storage: Storage) -> std::io::Resu let app_config_data = web::Data::new(config.clone()); let app_storage_data = web::Data::new(storage.clone()); // Clone for app_data + /* to avoid re-initialize the client at every api request */ + let docker_state = web::Data::new(Mutex::new(None::)); + // TODO: Put the secret here from the config let jwt_secret = config.server.secret.clone(); let jwt_config = web::Data::new(JwtConfig { @@ -89,6 +98,7 @@ pub async fn start_webserver(config: &Config, storage: Storage) -> std::io::Resu .app_data(app_config_data.clone()) // Share config .app_data(app_storage_data.clone()) // Share storage .app_data(jwt_config.clone()) // Share JWT config + .app_data(docker_state.clone()) // Public routes .service( web::scope("/api/v1/auth") diff --git a/crates/teus-docker/src/errors.rs b/crates/teus-docker/src/errors.rs new file mode 100644 index 0000000..759749a --- /dev/null +++ b/crates/teus-docker/src/errors.rs @@ -0,0 +1,31 @@ +use std::fmt::Display; + +use actix_web::ResponseError; +use docker::docker::DockerError; + +#[derive(Debug)] +pub enum ApiError { + Docker(DockerError), + Serialization(serde_qs::Error), + ServiceUnavailable(String), +} + +impl Display for ApiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ApiError::Docker(e) => write!(f, "An error occurred with the Docker API: {:?}", e), + ApiError::Serialization(e) => write!(f, "Failed to serialize query parameters: {}", e), + ApiError::ServiceUnavailable(msg) => write!(f, "{}", msg), + } + } +} + +impl ResponseError for ApiError { + fn status_code(&self) -> actix_web::http::StatusCode { + match self { + ApiError::Docker(_) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + ApiError::Serialization(_) => actix_web::http::StatusCode::BAD_REQUEST, + ApiError::ServiceUnavailable(_) => actix_web::http::StatusCode::SERVICE_UNAVAILABLE, + } + } +} diff --git a/crates/teus-docker/src/handlers.rs b/crates/teus-docker/src/handlers.rs index f9e0fa7..a73c1a4 100644 --- a/crates/teus-docker/src/handlers.rs +++ b/crates/teus-docker/src/handlers.rs @@ -1,9 +1,31 @@ -use std::fmt::Debug; - -use actix_web::{HttpResponse, Responder, get, web}; -use docker::docker::DockerClient; +use crate::errors::ApiError; +use actix_web::{HttpResponse, get, web}; +use docker::docker::{DockerClient, DockerError}; use serde::{Deserialize, Serialize}; use serde_qs::to_string; +use std::{fmt::Debug, sync::Mutex}; + +/* alias to get the life easier */ +type DockerState = web::Data>>; + +/* get the docker client from the state */ +fn get_client(state: &DockerState) -> Result { + let mut guard = state.lock().unwrap(); // lock the mutex + + if let Some(client) = guard.as_ref() { + return Ok(client.clone()); + } + + match DockerClient::new(None) { + Ok(client) => { + *guard = Some(client.clone()); + Ok(client) + } + Err(_) => Err(DockerError::Generic( + "Could not connect to the Docker socket. Is Docker running?".to_string(), + )), + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenericDockerResponse { @@ -16,69 +38,64 @@ struct ContainersQuery { } #[get("/docker/version")] -async fn get_docker_version() -> impl Responder { - let mut docker_client = DockerClient::new(None); +async fn get_docker_version(docker_state: DockerState) -> Result { + let mut docker_client = get_client(&docker_state).map_err(|e| ApiError::Docker(e))?; + match docker_client.get_version() { - Ok(version) => HttpResponse::Ok().json(version), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker version: {:?}", err), - }), + Ok(version) => Ok(HttpResponse::Ok().json(version)), + Err(err) => Err(ApiError::Docker(err)), } } #[get("/docker/containers")] -async fn get_docker_containers(query: web::Query) -> impl Responder { - println!("Query: {:?}", query); - +async fn get_docker_containers( + query: web::Query, + docker_state: DockerState, +) -> Result { let query_params: ContainersQuery = query.into_inner(); let query_string = to_string(&query_params).unwrap(); - - println!("Query: {:?}", query_string); - let mut docker_client = DockerClient::new(None); + let mut docker_client = get_client(&docker_state).map_err(|e| ApiError::Docker(e))?; match docker_client.get_containers(Some(query_string)) { - Ok(containers) => HttpResponse::Ok().json(containers), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker containers: {:?}", err), - }), + Ok(containers) => Ok(HttpResponse::Ok().json(containers)), + Err(err) => Err(ApiError::Docker(err)), } } #[get("/docker/container/{id}")] -async fn get_docker_container(id: web::Path) -> impl Responder { - let mut docker_client = DockerClient::new(None); +async fn get_docker_container( + id: web::Path, + docker_state: DockerState, +) -> Result { + let mut docker_client = get_client(&docker_state).map_err(|e| ApiError::Docker(e))?; let container_id_clone = id.clone(); match docker_client.get_container_details(container_id_clone) { - Ok(container) => HttpResponse::Ok().json(container), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker container {}: {:?}", id, err), - }), + Ok(container) => Ok(HttpResponse::Ok().json(container)), + Err(err) => Err(ApiError::Docker(err)), } } #[get("/docker/volumes")] -async fn get_docker_volumes() -> impl Responder { - let mut docker_client = DockerClient::new(None); +async fn get_docker_volumes(docker_state: DockerState) -> Result { + let mut docker_client = get_client(&docker_state).map_err(|e| ApiError::Docker(e))?; + match docker_client.get_volumes() { - Ok(volumes) => HttpResponse::Ok().json(volumes), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker volumes: {:?}", err), - }), + Ok(volumes) => Ok(HttpResponse::Ok().json(volumes)), + Err(err) => Err(ApiError::Docker(err)), } } -// FIXME: The id is not being passed correctly -// I think the problem is in the enum DockerApi #[get("/docker/volumes/{id}")] -async fn get_docker_volume(id: web::Path) -> impl Responder { - let mut docker_client = DockerClient::new(None); +async fn get_docker_volume( + id: web::Path, + docker_state: DockerState, +) -> Result { + let mut docker_client = get_client(&docker_state).map_err(|e| ApiError::Docker(e))?; let cloned_id = id.clone(); match docker_client.get_volume_details(cloned_id) { - Ok(volume) => HttpResponse::Ok().json(volume), - Err(err) => HttpResponse::InternalServerError().json(GenericDockerResponse { - message: format!("Error getting docker volume {} details: {:?}", id, err), - }), + Ok(volume) => Ok(HttpResponse::Ok().json(volume)), + Err(err) => Err(ApiError::Docker(err)), } } diff --git a/crates/teus-docker/src/lib.rs b/crates/teus-docker/src/lib.rs index c3d4495..0e2fecc 100644 --- a/crates/teus-docker/src/lib.rs +++ b/crates/teus-docker/src/lib.rs @@ -1 +1,2 @@ -pub mod handlers; +pub mod errors; +pub mod handlers; \ No newline at end of file From 40514160d868b2771eb27fcc7889573f4bf45db4 Mon Sep 17 00:00:00 2001 From: Giovanni D'Andrea Date: Thu, 10 Jul 2025 01:38:47 +0200 Subject: [PATCH 20/21] Enhance install script to check if user 'teus' is in the 'docker' group and prompt to add if not. Simplify sudo privilege error message. --- install.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index e8bb6a0..dc92c12 100755 --- a/install.sh +++ b/install.sh @@ -78,7 +78,20 @@ echo -e "========================================${NC}" # Check for sudo privileges if [[ $EUID -ne 0 && "$(id -u)" -ne 0 ]]; then if ! command -v sudo >/dev/null 2>&1; then - error "This script requires sudo privileges. Please run as root or install sudo." + error "This script requires sudo privileges." + fi +fi + +# Check if the user teus is inside docker group +if ! id -nG teus | grep -qw "docker"; then + warning "The user 'teus' is not in the 'docker' group. Please add the user to the docker group and try again." + read -rp "Do you want to add the user 'teus' to the docker group? [Y/n] " add_to_docker_group + add_to_docker_group=${add_to_docker_group:-Y} + if [[ $add_to_docker_group =~ ^[Yy]$ ]]; then + sudo usermod -aG docker teus + success "User 'teus' added to docker group." + else + error "User 'teus' is not in the docker group. Please add the user to the docker group and try again." fi fi From e457666021e1d828c480f7ea200be0378b8036b7 Mon Sep 17 00:00:00 2001 From: Giovanni D'Andrea Date: Wed, 16 Jul 2025 02:09:48 +0200 Subject: [PATCH 21/21] Add important disclaimer to README regarding active development status and potential instability of the software --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 181b215..f7fed5a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,19 @@ [![Rust Teus CI](https://github.com/imggion/Teus/actions/workflows/rust.yml/badge.svg)](https://github.com/imggion/Teus/actions/workflows/rust.yml) +## ⚠️ Important Disclaimer + +**This software is currently under active development and is NOT ready for production use.** + +Please be aware that: +- The codebase is subject to frequent changes and breaking updates +- Features may be incomplete or unstable +- Database schemas and APIs may change without notice +- Security features may not be fully implemented or tested +- No guarantees are provided regarding data integrity or system stability +- Refactoring needed + + Teus is a lightweight system monitoring service written in Rust that collects and exposes system metrics through a REST API. ## Features