From f0fdba0ff67d75a4fac1443fa45528389d4f27aa Mon Sep 17 00:00:00 2001 From: "Ivan I. Ovchinnikov" Date: Tue, 17 Mar 2026 22:03:13 +0300 Subject: [PATCH] multi user ready --- Cargo.lock | 328 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/handlers/mod.rs | 270 ++++++++++++++++++++++++++++----- src/main.rs | 16 +- templates/index.html | 33 +++- templates/settings.html | 161 ++++++++++++++++++++ 6 files changed, 763 insertions(+), 48 deletions(-) create mode 100644 templates/settings.html diff --git a/Cargo.lock b/Cargo.lock index ac93c06..0a8e54e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,12 +150,42 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "fastrand", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "multer", + "pin-project-lite", + "serde", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "basic-toml" version = "0.1.10" @@ -184,6 +214,7 @@ dependencies = [ "askama", "askama_axum", "axum", + "axum-extra", "dotenvy", "reqwest", "serde", @@ -191,6 +222,7 @@ dependencies = [ "tokio", "tower 0.4.13", "tower-http", + "tower-sessions", "tracing", "tracing-subscriber", ] @@ -209,9 +241,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -223,6 +255,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -249,6 +292,16 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -339,6 +392,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -346,6 +413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -354,6 +422,23 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -373,11 +458,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-macro", + "futures-sink", "futures-task", "pin-project-lite", "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -771,6 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", + "serde", ] [[package]] @@ -833,6 +932,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.4.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -869,6 +985,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-traits" version = "0.2.19" @@ -984,6 +1106,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1018,6 +1155,36 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1050,7 +1217,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1103,7 +1270,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -1285,6 +1452,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1353,12 +1526,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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 = "thread_local" version = "1.1.9" @@ -1368,6 +1561,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -1456,6 +1680,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http 1.4.0", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -1493,6 +1734,57 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50571505955aaa8b73f2f40489953d92b4d7ff9eb9b2a8b4e11fee0dcdb2760e" +dependencies = [ + "async-trait", + "http 1.4.0", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6293bf33f1977d5ef422c2e02f909eb2c3d7bf921d93557c40d4f1b130b84aa4" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.22.1", + "futures", + "http 1.4.0", + "parking_lot", + "rand", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec5f88eeef0f036e6900217034efbce733cbdf0528a85204eaaed90bc34c354" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.44" @@ -1609,6 +1901,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -2026,6 +2324,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 9eb857c..df04062 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Шаблонизатор (будем рендерить HTML на сервере) askama = "0.12" askama_axum = "0.4" + +axum-extra = { version = "0.9", features = ["cookie"] } +tower-sessions = { version = "0.12", features = ["memory-store"] } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 715d50e..03d395d 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -2,16 +2,20 @@ use crate::models::{JournalDetail, Task, TaskAttachment, TaskDetail, TaskJournal use crate::services::redmine; use askama::Template; use axum::{ - extract::Path, + Form, + extract::{Path, State}, response::{Html, IntoResponse, Json}, }; +use serde::{Deserialize, Serialize}; use serde_json::json; +use tower_sessions::Session; #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate { issues: Vec, error: Option, + has_settings: bool, } #[derive(Template)] @@ -21,10 +25,33 @@ struct TaskDetailTemplate { error: Option, } -pub async fn index() -> impl IntoResponse { +#[derive(Template)] +#[template(path = "settings.html")] +struct SettingsTemplate { + redmine_url: String, + redmine_user_id: String, + error: Option, + success: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct UserSettings { + pub redmine_url: String, + pub redmine_api_key: String, + pub redmine_user_id: u32, +} + +pub async fn index(session: Session) -> impl IntoResponse { + let has_settings = session + .get::("user_settings") + .await + .unwrap_or(None) + .is_some(); + let template = IndexTemplate { issues: Vec::new(), error: None, + has_settings, }; Html( template @@ -33,19 +60,130 @@ pub async fn index() -> impl IntoResponse { ) } -pub async fn test_redmine() -> impl IntoResponse { - let url = std::env::var("REDMINE_URL") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - let api_key = std::env::var("REDMINE_API_KEY").unwrap_or_default(); - let user_id: u32 = std::env::var("REDMINE_USER_ID") - .unwrap_or_default() - .parse() - .unwrap_or(0); +pub async fn settings_page(session: Session) -> impl IntoResponse { + let settings = session + .get::("user_settings") + .await + .unwrap_or(None); - match redmine::fetch_user_issues(&api_key, &url, user_id).await { + let template = SettingsTemplate { + redmine_url: settings + .as_ref() + .map(|s| s.redmine_url.clone()) + .unwrap_or_default(), + redmine_user_id: settings + .as_ref() + .map(|s| s.redmine_user_id.to_string()) + .unwrap_or_default(), + error: None, + success: None, + }; + Html( + template + .render() + .unwrap_or_else(|e| format!("Error: {}", e)), + ) +} + +#[derive(Debug, Deserialize)] +pub struct SettingsForm { + pub redmine_url: String, + pub redmine_api_key: String, + pub redmine_user_id: String, +} + +pub async fn save_settings(session: Session, Form(form): Form) -> impl IntoResponse { + // Валидация + if form.redmine_url.is_empty() + || form.redmine_api_key.is_empty() + || form.redmine_user_id.is_empty() + { + let template = SettingsTemplate { + redmine_url: form.redmine_url, + redmine_user_id: form.redmine_user_id, + error: Some("Все поля обязательны для заполнения".to_string()), + success: None, + }; + return Html( + template + .render() + .unwrap_or_else(|e| format!("Error: {}", e)), + ); + } + + let user_id: u32 = match form.redmine_user_id.parse() { + Ok(id) => id, + Err(_) => { + let template = SettingsTemplate { + redmine_url: form.redmine_url, + redmine_user_id: form.redmine_user_id, + error: Some("User ID должен быть числом".to_string()), + success: None, + }; + return Html( + template + .render() + .unwrap_or_else(|e| format!("Error: {}", e)), + ); + } + }; + + let settings = UserSettings { + redmine_url: form.redmine_url.trim().trim_matches('"').to_string(), + redmine_api_key: form.redmine_api_key, + redmine_user_id: user_id, + }; + + // Сохраняем в сессию + if let Err(e) = session.insert("user_settings", settings).await { + let template = SettingsTemplate { + redmine_url: form.redmine_url, + redmine_user_id: form.redmine_user_id, + error: Some(format!("Ошибка сохранения: {}", e)), + success: None, + }; + return Html( + template + .render() + .unwrap_or_else(|e| format!("Error: {}", e)), + ); + } + + let template = SettingsTemplate { + redmine_url: form.redmine_url, + redmine_user_id: form.redmine_user_id.to_string(), + error: None, + success: Some("Настройки сохранены! Теперь можно загружать задачи.".to_string()), + }; + Html( + template + .render() + .unwrap_or_else(|e| format!("Error: {}", e)), + ) +} + +pub async fn test_redmine(session: Session) -> impl IntoResponse { + let settings = match session + .get::("user_settings") + .await + .unwrap_or(None) + { + Some(s) => s, + None => { + return Json(json!({ + "status": "error", + "message": "Сначала настройте подключение в разделе Настройки" + })); + } + }; + + match redmine::fetch_user_issues( + &settings.redmine_api_key, + &settings.redmine_url, + settings.redmine_user_id, + ) + .await + { Ok(issues) => Json(json!({ "status": "success", "count": issues.len(), @@ -58,23 +196,45 @@ pub async fn test_redmine() -> impl IntoResponse { } } -pub async fn get_redmine_tasks() -> impl IntoResponse { +pub async fn get_redmine_tasks(session: Session) -> impl IntoResponse { tracing::info!("Запрос задач из Redmine"); - let url = std::env::var("REDMINE_URL") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - let api_key = std::env::var("REDMINE_API_KEY").unwrap_or_default(); - let user_id: u32 = std::env::var("REDMINE_USER_ID") - .unwrap_or_default() - .parse() - .unwrap_or(0); + let settings = match session + .get::("user_settings") + .await + .unwrap_or(None) + { + Some(s) => s, + None => { + let template = IndexTemplate { + issues: Vec::new(), + error: Some( + "Сначала настройте подключение в разделе Настройки" + .to_string(), + ), + has_settings: false, + }; + return Html( + template + .render() + .unwrap_or_else(|e| format!("Error: {}", e)), + ); + } + }; - tracing::info!("Redmine URL: {}, User ID: {}", url, user_id); + tracing::info!( + "Redmine URL: {}, User ID: {}", + settings.redmine_url, + settings.redmine_user_id + ); - match redmine::fetch_user_issues(&api_key, &url, user_id).await { + match redmine::fetch_user_issues( + &settings.redmine_api_key, + &settings.redmine_url, + settings.redmine_user_id, + ) + .await + { Ok(issues) => { tracing::info!("Получено задач: {}", issues.len()); @@ -106,6 +266,7 @@ pub async fn get_redmine_tasks() -> impl IntoResponse { let template = IndexTemplate { issues: tasks, error: None, + has_settings: true, }; let rendered = template.render(); @@ -122,6 +283,7 @@ pub async fn get_redmine_tasks() -> impl IntoResponse { let template = IndexTemplate { issues: Vec::new(), error: Some(e.to_string()), + has_settings: true, }; Html( template @@ -136,6 +298,7 @@ pub async fn get_bitrix_tasks() -> impl IntoResponse { let template = IndexTemplate { issues: Vec::new(), error: Some("Bitrix24 интеграция в разработке".to_string()), + has_settings: false, }; Html( template @@ -144,17 +307,54 @@ pub async fn get_bitrix_tasks() -> impl IntoResponse { ) } -pub async fn get_task_detail(Path(issue_id): Path) -> impl IntoResponse { +pub async fn get_task_detail(Path(issue_id): Path, session: Session) -> impl IntoResponse { tracing::info!("Запрос детали задачи #{}", issue_id); - let url = std::env::var("REDMINE_URL") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - let api_key = std::env::var("REDMINE_API_KEY").unwrap_or_default(); + let settings = match session + .get::("user_settings") + .await + .unwrap_or(None) + { + Some(s) => s, + None => { + let template = TaskDetailTemplate { + task: TaskDetail { + id: issue_id, + source: "redmine".to_string(), + subject: String::new(), + description: String::new(), + status_name: String::new(), + status_class: "closed".to_string(), + priority_name: "—".to_string(), + project_name: "—".to_string(), + author_name: "—".to_string(), + assigned_to_name: "—".to_string(), + created_on: String::new(), + updated_on: String::new(), + start_date: String::new(), + due_date: String::new(), + done_ratio: 0, + estimated_hours: 0.0, + spent_hours: 0.0, + journals: Vec::new(), + attachments: Vec::new(), + }, + error: Some( + "Сначала настройте подключение в разделе Настройки" + .to_string(), + ), + }; + return Html( + template + .render() + .unwrap_or_else(|e| format!("Error: {}", e)), + ); + } + }; - match redmine::fetch_issue_full(&api_key, &url, issue_id).await { + match redmine::fetch_issue_full(&settings.redmine_api_key, &settings.redmine_url, issue_id) + .await + { Ok(issue) => { tracing::info!("Получена задача #{}", issue_id); diff --git a/src/main.rs b/src/main.rs index 39117a2..6f9000a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use axum::{Router, routing::get}; use std::net::SocketAddr; use tokio::net::TcpListener; +use tower_sessions::MemoryStore; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -// Объявление модулей pub mod config; pub mod handlers; pub mod models; @@ -11,25 +11,29 @@ pub mod services; #[tokio::main] async fn main() { - // Инициализация логгера tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), )) .init(); - // Загрузка переменных окружения dotenvy::dotenv().ok(); - // Маршрутизация + // Создаём хранилище сессий в памяти + let session_store = MemoryStore::default(); + let app = Router::new() .route("/", get(handlers::index)) + .route( + "/settings", + get(handlers::settings_page).post(handlers::save_settings), + ) .route("/api/redmine", get(handlers::test_redmine)) .route("/api/redmine/tasks", get(handlers::get_redmine_tasks)) .route("/api/bitrix/tasks", get(handlers::get_bitrix_tasks)) - .route("/task/:id", get(handlers::get_task_detail)); + .route("/task/:id", get(handlers::get_task_detail)) + .layer(tower_sessions::SessionManagerLayer::new(session_store)); - // Запуск сервера (axum 0.7 API) let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); tracing::info!("Listening on {}", addr); diff --git a/templates/index.html b/templates/index.html index ee34870..34c046c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,6 +13,12 @@ } .container { max-width: 1400px; margin: 0 auto; } h1 { color: #333; margin-bottom: 20px; } + .header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } .controls { background: white; padding: 20px; @@ -36,6 +42,8 @@ .btn:disabled { background: #ccc; cursor: not-allowed; } .btn-bitrix { background: #00aeef; } .btn-bitrix:hover { background: #008cc7; } + .btn-settings { background: #6c757d; } + .btn-settings:hover { background: #545b62; } table { width: 100%; background: white; @@ -83,14 +91,35 @@ text-overflow: ellipsis; white-space: nowrap; } + .setup-notice { + background: #fff3e0; + border-left: 4px solid #f57c00; + padding: 16px; + margin-bottom: 20px; + border-radius: 0 8px 8px 0; + } + .setup-notice a { color: #e65100; font-weight: 600; }
-

🔗 Bitmine — Агрегатор задач

+
+

🔗 Bitmine — Агрегатор задач

+ ⚙️ Настройки +
+ + {% if !has_settings %} +
+ ⚠️ Требуется настройка! + Сначала укажите данные для подключения к Redmine в разделе + Настройки. +
+ {% endif %} diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..18f082a --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,161 @@ + + + + + + Настройки — Bitmine + + + +
+ ← Назад к списку задач + +

⚙️ Настройки подключения

+ +
+ {% if error.is_some() %} +
{{ error.as_ref().unwrap() }}
+ {% endif %} + + {% if success.is_some() %} +
{{ success.as_ref().unwrap() }}
+ {% endif %} + +
+
+ + +
Полный URL вашего Redmine, например: https://redmine.company.com
+
+ +
+ + +
Ключ можно получить в Настройки → Мой аккаунт → API access key
+
+ +
+ + +
Ваш ID пользователя в Redmine (видно в профиле или URL)
+
+ + + Отмена +
+ +
+ 🔒 Безопасность: Ваши учётные данные хранятся только в сессии браузера и не сохраняются на сервере. + При закрытии браузера сессия истекает и данные удаляются. +
+
+
+ +