multi user ready
This commit is contained in:
parent
db6a12bb5f
commit
f0fdba0ff6
328
Cargo.lock
generated
328
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"] }
|
||||
|
||||
@ -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<Task>,
|
||||
error: Option<String>,
|
||||
has_settings: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -21,10 +25,33 @@ struct TaskDetailTemplate {
|
||||
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 {
|
||||
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::<UserSettings>("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<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!({
|
||||
"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::<UserSettings>("user_settings")
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
{
|
||||
Some(s) => s,
|
||||
None => {
|
||||
let template = IndexTemplate {
|
||||
issues: Vec::new(),
|
||||
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) => {
|
||||
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<u32>) -> impl IntoResponse {
|
||||
pub async fn get_task_detail(Path(issue_id): Path<u32>, 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::<UserSettings>("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(
|
||||
"Сначала настройте подключение в разделе <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) => {
|
||||
tracing::info!("Получена задача #{}", issue_id);
|
||||
|
||||
|
||||
16
src/main.rs
16
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);
|
||||
|
||||
|
||||
@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
161
templates/settings.html
Normal file
161
templates/settings.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user