multi user ready

This commit is contained in:
Ivan I. Ovchinnikov 2026-03-17 22:03:13 +03:00
parent db6a12bb5f
commit f0fdba0ff6
6 changed files with 763 additions and 48 deletions

328
Cargo.lock generated
View File

@ -150,12 +150,42 @@ dependencies = [
"tracing", "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]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "basic-toml" name = "basic-toml"
version = "0.1.10" version = "0.1.10"
@ -184,6 +214,7 @@ dependencies = [
"askama", "askama",
"askama_axum", "askama_axum",
"axum", "axum",
"axum-extra",
"dotenvy", "dotenvy",
"reqwest", "reqwest",
"serde", "serde",
@ -191,6 +222,7 @@ dependencies = [
"tokio", "tokio",
"tower 0.4.13", "tower 0.4.13",
"tower-http", "tower-http",
"tower-sessions",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@ -209,9 +241,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.56" version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@ -223,6 +255,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -249,6 +292,16 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 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]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@ -339,6 +392,20 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.32" version = "0.3.32"
@ -346,6 +413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@ -354,6 +422,23 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 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]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.32" version = "0.3.32"
@ -373,11 +458,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-macro",
"futures-sink",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
"slab", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.4.2" version = "0.4.2"
@ -771,6 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
"serde",
] ]
[[package]] [[package]]
@ -833,6 +932,23 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.18" version = "0.2.18"
@ -869,6 +985,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -984,6 +1106,21 @@ dependencies = [
"zerovec", "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]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@ -1018,6 +1155,36 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@ -1050,7 +1217,7 @@ version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [ dependencies = [
"base64", "base64 0.21.7",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -1103,7 +1270,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [ dependencies = [
"base64", "base64 0.21.7",
] ]
[[package]] [[package]]
@ -1285,6 +1452,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@ -1353,12 +1526,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "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]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.9" version = "1.1.9"
@ -1368,6 +1561,37 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.2"
@ -1456,6 +1680,23 @@ dependencies = [
"tracing", "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]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.5.2" version = "0.5.2"
@ -1493,6 +1734,57 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 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]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
@ -1609,6 +1901,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -2026,6 +2324,26 @@ dependencies = [
"synstructure", "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]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.6" version = "0.1.6"

View File

@ -29,3 +29,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Шаблонизатор (будем рендерить HTML на сервере) # Шаблонизатор (будем рендерить HTML на сервере)
askama = "0.12" askama = "0.12"
askama_axum = "0.4" askama_axum = "0.4"
axum-extra = { version = "0.9", features = ["cookie"] }
tower-sessions = { version = "0.12", features = ["memory-store"] }

View File

@ -2,16 +2,20 @@ use crate::models::{JournalDetail, Task, TaskAttachment, TaskDetail, TaskJournal
use crate::services::redmine; use crate::services::redmine;
use askama::Template; use askama::Template;
use axum::{ use axum::{
extract::Path, Form,
extract::{Path, State},
response::{Html, IntoResponse, Json}, response::{Html, IntoResponse, Json},
}; };
use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use tower_sessions::Session;
#[derive(Template)] #[derive(Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
struct IndexTemplate { struct IndexTemplate {
issues: Vec<Task>, issues: Vec<Task>,
error: Option<String>, error: Option<String>,
has_settings: bool,
} }
#[derive(Template)] #[derive(Template)]
@ -21,10 +25,33 @@ struct TaskDetailTemplate {
error: Option<String>, error: Option<String>,
} }
pub async fn index() -> impl IntoResponse { #[derive(Template)]
#[template(path = "settings.html")]
struct SettingsTemplate {
redmine_url: String,
redmine_user_id: String,
error: Option<String>,
success: Option<String>,
}
#[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::<UserSettings>("user_settings")
.await
.unwrap_or(None)
.is_some();
let template = IndexTemplate { let template = IndexTemplate {
issues: Vec::new(), issues: Vec::new(),
error: None, error: None,
has_settings,
}; };
Html( Html(
template template
@ -33,19 +60,130 @@ pub async fn index() -> impl IntoResponse {
) )
} }
pub async fn test_redmine() -> impl IntoResponse { pub async fn settings_page(session: Session) -> impl IntoResponse {
let url = std::env::var("REDMINE_URL") let settings = session
.unwrap_or_default() .get::<UserSettings>("user_settings")
.trim() .await
.trim_matches('"') .unwrap_or(None);
.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);
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<SettingsForm>) -> 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::<UserSettings>("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!({ Ok(issues) => Json(json!({
"status": "success", "status": "success",
"count": issues.len(), "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"); tracing::info!("Запрос задач из Redmine");
let url = std::env::var("REDMINE_URL") let settings = match session
.unwrap_or_default() .get::<UserSettings>("user_settings")
.trim() .await
.trim_matches('"') .unwrap_or(None)
.to_string(); {
let api_key = std::env::var("REDMINE_API_KEY").unwrap_or_default(); Some(s) => s,
let user_id: u32 = std::env::var("REDMINE_USER_ID") None => {
.unwrap_or_default() let template = IndexTemplate {
.parse() issues: Vec::new(),
.unwrap_or(0); error: Some(
"Сначала настройте подключение в разделе <a href='/settings'>Настройки</a>"
.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) => { Ok(issues) => {
tracing::info!("Получено задач: {}", issues.len()); tracing::info!("Получено задач: {}", issues.len());
@ -106,6 +266,7 @@ pub async fn get_redmine_tasks() -> impl IntoResponse {
let template = IndexTemplate { let template = IndexTemplate {
issues: tasks, issues: tasks,
error: None, error: None,
has_settings: true,
}; };
let rendered = template.render(); let rendered = template.render();
@ -122,6 +283,7 @@ pub async fn get_redmine_tasks() -> impl IntoResponse {
let template = IndexTemplate { let template = IndexTemplate {
issues: Vec::new(), issues: Vec::new(),
error: Some(e.to_string()), error: Some(e.to_string()),
has_settings: true,
}; };
Html( Html(
template template
@ -136,6 +298,7 @@ pub async fn get_bitrix_tasks() -> impl IntoResponse {
let template = IndexTemplate { let template = IndexTemplate {
issues: Vec::new(), issues: Vec::new(),
error: Some("Bitrix24 интеграция в разработке".to_string()), error: Some("Bitrix24 интеграция в разработке".to_string()),
has_settings: false,
}; };
Html( Html(
template template
@ -144,17 +307,54 @@ pub async fn get_bitrix_tasks() -> impl IntoResponse {
) )
} }
pub async fn get_task_detail(Path(issue_id): Path<u32>) -> impl IntoResponse { pub async fn get_task_detail(Path(issue_id): Path<u32>, session: Session) -> impl IntoResponse {
tracing::info!("Запрос детали задачи #{}", issue_id); tracing::info!("Запрос детали задачи #{}", issue_id);
let url = std::env::var("REDMINE_URL") let settings = match session
.unwrap_or_default() .get::<UserSettings>("user_settings")
.trim() .await
.trim_matches('"') .unwrap_or(None)
.to_string(); {
let api_key = std::env::var("REDMINE_API_KEY").unwrap_or_default(); 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(
"Сначала настройте подключение в разделе <a href='/settings'>Настройки</a>"
.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) => { Ok(issue) => {
tracing::info!("Получена задача #{}", issue_id); tracing::info!("Получена задача #{}", issue_id);

View File

@ -1,9 +1,9 @@
use axum::{Router, routing::get}; use axum::{Router, routing::get};
use std::net::SocketAddr; use std::net::SocketAddr;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_sessions::MemoryStore;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// Объявление модулей
pub mod config; pub mod config;
pub mod handlers; pub mod handlers;
pub mod models; pub mod models;
@ -11,25 +11,29 @@ pub mod services;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Инициализация логгера
tracing_subscriber::registry() tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new( .with(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
)) ))
.init(); .init();
// Загрузка переменных окружения
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
// Маршрутизация // Создаём хранилище сессий в памяти
let session_store = MemoryStore::default();
let app = Router::new() let app = Router::new()
.route("/", get(handlers::index)) .route("/", get(handlers::index))
.route(
"/settings",
get(handlers::settings_page).post(handlers::save_settings),
)
.route("/api/redmine", get(handlers::test_redmine)) .route("/api/redmine", get(handlers::test_redmine))
.route("/api/redmine/tasks", get(handlers::get_redmine_tasks)) .route("/api/redmine/tasks", get(handlers::get_redmine_tasks))
.route("/api/bitrix/tasks", get(handlers::get_bitrix_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)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("Listening on {}", addr); tracing::info!("Listening on {}", addr);

View File

@ -13,6 +13,12 @@
} }
.container { max-width: 1400px; margin: 0 auto; } .container { max-width: 1400px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; } h1 { color: #333; margin-bottom: 20px; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.controls { .controls {
background: white; background: white;
padding: 20px; padding: 20px;
@ -36,6 +42,8 @@
.btn:disabled { background: #ccc; cursor: not-allowed; } .btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-bitrix { background: #00aeef; } .btn-bitrix { background: #00aeef; }
.btn-bitrix:hover { background: #008cc7; } .btn-bitrix:hover { background: #008cc7; }
.btn-settings { background: #6c757d; }
.btn-settings:hover { background: #545b62; }
table { table {
width: 100%; width: 100%;
background: white; background: white;
@ -83,14 +91,35 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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; }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>🔗 Bitmine — Агрегатор задач</h1> <div class="header">
<h1>🔗 Bitmine — Агрегатор задач</h1>
<a href="/settings" class="btn btn-settings">⚙️ Настройки</a>
</div>
{% if !has_settings %}
<div class="setup-notice">
⚠️ <strong>Требуется настройка!</strong>
Сначала укажите данные для подключения к Redmine в разделе
<a href="/settings">Настройки</a>.
</div>
{% endif %}
<div class="controls"> <div class="controls">
<a href="/api/redmine/tasks" class="btn">📋 Получить задачи из Redmine</a> <a href="/api/redmine/tasks" class="btn {% if !has_settings %}btn-disabled{% endif %}">
📋 Получить задачи из Redmine
</a>
<button class="btn btn-bitrix" disabled>📌 Получить задачи из Bitrix24</button> <button class="btn btn-bitrix" disabled>📌 Получить задачи из Bitrix24</button>
</div> </div>

161
templates/settings.html Normal file
View File

@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Настройки — Bitmine</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container { max-width: 600px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 24px;
}
.form-group { margin-bottom: 20px; }
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
input[type="text"],
input[type="password"],
input[type="number"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
}
.help-text {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn:hover { background: #0056b3; }
.btn-secondary {
background: #6c757d;
margin-left: 10px;
}
.btn-secondary:hover { background: #545b62; }
.error {
background: #ffebee;
color: #c62828;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.success {
background: #e8f5e9;
color: #388e3c;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
.security-note {
background: #fff3e0;
border-left: 4px solid #f57c00;
padding: 12px;
margin-top: 20px;
font-size: 13px;
color: #e65100;
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← Назад к списку задач</a>
<h1>⚙️ Настройки подключения</h1>
<div class="card">
{% if error.is_some() %}
<div class="error">{{ error.as_ref().unwrap() }}</div>
{% endif %}
{% if success.is_some() %}
<div class="success">{{ success.as_ref().unwrap() }}</div>
{% endif %}
<form method="POST" action="/settings">
<div class="form-group">
<label for="redmine_url">Redmine URL</label>
<input
type="text"
id="redmine_url"
name="redmine_url"
value="{{ redmine_url }}"
placeholder="https://redmine.example.com"
required
>
<div class="help-text">Полный URL вашего Redmine, например: https://redmine.company.com</div>
</div>
<div class="form-group">
<label for="redmine_api_key">API Key</label>
<input
type="password"
id="redmine_api_key"
name="redmine_api_key"
value=""
placeholder="Ваш API ключ из настроек Redmine"
required
>
<div class="help-text">Ключ можно получить в Настройки → Мой аккаунт → API access key</div>
</div>
<div class="form-group">
<label for="redmine_user_id">User ID</label>
<input
type="number"
id="redmine_user_id"
name="redmine_user_id"
value="{{ redmine_user_id }}"
placeholder="7"
required
>
<div class="help-text">Ваш ID пользователя в Redmine (видно в профиле или URL)</div>
</div>
<button type="submit" class="btn">💾 Сохранить настройки</button>
<a href="/" class="btn btn-secondary">Отмена</a>
</form>
<div class="security-note">
🔒 <strong>Безопасность:</strong> Ваши учётные данные хранятся только в сессии браузера и не сохраняются на сервере.
При закрытии браузера сессия истекает и данные удаляются.
</div>
</div>
</div>
</body>
</html>