diff --git a/.env.example b/.env.example index b7bbd0e..9952aa5 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ BINANCE_API_KEY=your-live-api-key BINANCE_API_SECRET=your-live-api-secret BINANCE_TESTNET_API_KEY=your-testnet-api-key BINANCE_TESTNET_API_SECRET=your-testnet-api-secret -AUTH_TOKEN=your-dashboard-access-token +# bcrypt hash — üretmek için: htpasswd -bnBC 10 "" sifreniz | tr -d ':\n' | sed 's/$2y/$2b/' +ADMIN_PASSWORD_HASH=$2b$10$example_hash_here DB_PATH=data/bots.db LISTEN_ADDR=127.0.0.1:4646 diff --git a/Cargo.lock b/Cargo.lock index a7b1a7c..e7b6876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,28 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -167,6 +189,30 @@ 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", + "http-body", + "http-body-util", + "mime", + "multer", + "pin-project-lite", + "serde", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-macros" version = "0.4.2" @@ -184,6 +230,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcrypt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +dependencies = [ + "base64", + "blowfish", + "getrandom 0.2.17", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "2.11.1" @@ -199,6 +258,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -253,12 +322,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[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.10.1" @@ -300,6 +390,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -328,6 +427,15 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_filter" version = "1.0.1" @@ -379,6 +487,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -812,6 +926,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -970,7 +1093,10 @@ name = "mukan-special-edition" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "axum", + "axum-extra", + "bcrypt", "chrono", "dotenvy", "env_logger", @@ -992,6 +1118,29 @@ dependencies = [ "uuid", ] +[[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", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -1084,6 +1233,12 @@ 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" @@ -1586,6 +1741,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" @@ -1669,6 +1830,37 @@ dependencies = [ "syn", ] +[[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.3" diff --git a/Cargo.toml b/Cargo.toml index 6112607..5f23b78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,3 +54,10 @@ futures-core = "0.3" # WebSocket tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] } futures-util = "0.3" + +# Auth +bcrypt = "0.15" +axum-extra = { version = "0.9", features = ["cookie"] } + +# Async stream generator +async-stream = "0.3" diff --git a/src/api/auth.rs b/src/api/auth.rs index e9d14c1..7680d44 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -1,35 +1,97 @@ use axum::{ - extract::{Query, Request, State}, + extract::{Request, State}, http::StatusCode, middleware::Next, - response::Response, + response::{IntoResponse, Response}, + Json, }; -use serde::Deserialize; +use axum_extra::extract::CookieJar; +use axum_extra::extract::cookie::{Cookie, SameSite}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::AppState; +const SESSION_COOKIE: &str = "mse_session"; + #[derive(Deserialize)] -pub struct TokenQuery { - pub token: Option, +pub struct LoginRequest { + pub password: String, +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub ok: bool, +} + +pub async fn login( + State(state): State, + jar: CookieJar, + Json(req): Json, +) -> Result<(CookieJar, impl IntoResponse), StatusCode> { + let valid = bcrypt::verify(&req.password, &state.password_hash) + .unwrap_or(false); + + if !valid { + return Err(StatusCode::UNAUTHORIZED); + } + + let token = Uuid::new_v4().to_string(); + + { + let db = state.db.lock().await; + db.insert_session(&token).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } + + let cookie = Cookie::build((SESSION_COOKIE, token)) + .path("/") + .http_only(true) + .same_site(SameSite::Strict) + .build(); + + Ok((jar.add(cookie), Json(LoginResponse { ok: true }))) +} + +pub async fn logout( + State(state): State, + jar: CookieJar, +) -> impl IntoResponse { + if let Some(cookie) = jar.get(SESSION_COOKIE) { + let token = cookie.value().to_string(); + let db = state.db.lock().await; + db.delete_session(&token).ok(); + } + + let removal = Cookie::build((SESSION_COOKIE, "")) + .path("/") + .http_only(true) + .same_site(SameSite::Strict) + .build(); + + (jar.remove(removal), Json(LoginResponse { ok: true })) } pub async fn require_auth( State(state): State, - Query(query): Query, + jar: CookieJar, request: Request, next: Next, ) -> Result { - let header_token = request - .headers() - .get("authorization") - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.strip_prefix("Bearer ")) - .map(|s| s.to_string()); + let token = jar + .get(SESSION_COOKIE) + .map(|c| c.value().to_string()); - let token = header_token.or(query.token); - - match token.as_deref() { - Some(t) if t == state.auth_token => Ok(next.run(request).await), - _ => Err(StatusCode::UNAUTHORIZED), + match token { + Some(t) => { + let db = state.db.lock().await; + let exists = db.session_exists(&t).unwrap_or(false); + drop(db); + if exists { + Ok(next.run(request).await) + } else { + Err(StatusCode::UNAUTHORIZED) + } + } + None => Err(StatusCode::UNAUTHORIZED), } } diff --git a/src/api/bots.rs b/src/api/bots.rs index aaccc3c..90df7e2 100644 --- a/src/api/bots.rs +++ b/src/api/bots.rs @@ -156,3 +156,91 @@ pub async fn stop_bot( StatusCode::OK.into_response() } + +pub async fn get_bot( + State(state): State, + Path(id): Path, +) -> impl IntoResponse { + let db = state.db.lock().await; + let bots = match db.get_bots() { + Ok(b) => b, + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + }; + let config = match bots.into_iter().find(|b| b.id == id) { + Some(c) => c, + None => return StatusCode::NOT_FOUND.into_response(), + }; + drop(db); + let manager = state.manager.lock().await; + let running = manager.is_running(&id); + Json(BotResponse { config, running }).into_response() +} + +#[derive(Deserialize)] +pub struct UpdateBotRequest { + pub name: Option, + pub usdt_amount: Option, + pub profit_percent: Option, +} + +pub async fn update_bot( + State(state): State, + Path(id): Path, + Json(req): Json, +) -> impl IntoResponse { + let db = state.db.lock().await; + let bots = match db.get_bots() { + Ok(b) => b, + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + }; + let mut config = match bots.into_iter().find(|b| b.id == id) { + Some(c) => c, + None => return StatusCode::NOT_FOUND.into_response(), + }; + + if let Some(name) = req.name { config.name = name; } + if let Some(usdt) = req.usdt_amount { config.usdt_amount = usdt; } + if let Some(profit) = req.profit_percent { config.profit_percent = profit; } + + if let Err(e) = db.update_bot(&config) { + return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(); + } + drop(db); + + let manager = state.manager.lock().await; + let running = manager.is_running(&id); + Json(BotResponse { config, running }).into_response() +} + +pub async fn get_bot_logs( + State(state): State, + Path(id): Path, +) -> impl IntoResponse { + let db = state.db.lock().await; + match db.get_logs(&id, 200) { + Ok(logs) => Json(logs).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} + +pub async fn get_bot_positions( + State(state): State, + Path(id): Path, +) -> impl IntoResponse { + let db = state.db.lock().await; + match db.get_positions_by_bot(&id) { + Ok(p) => Json(p).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} + +pub async fn get_bot_closed( + State(state): State, + Path(id): Path, +) -> impl IntoResponse { + let db = state.db.lock().await; + match db.get_closed_by_bot(&id) { + Ok(p) => Json(p).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} diff --git a/src/api/events.rs b/src/api/events.rs index 0a4bd22..e242ad1 100644 --- a/src/api/events.rs +++ b/src/api/events.rs @@ -1,4 +1,8 @@ -use axum::{extract::State, response::sse::{Event, KeepAlive, Sse}}; +use axum::{ + extract::{Path, State}, + response::sse::{Event, KeepAlive, Sse}, +}; +use tokio::time::Duration; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use crate::AppState; @@ -16,3 +20,24 @@ pub async fn sse_handler( Sse::new(stream).keep_alive(KeepAlive::default()) } + +pub async fn bot_log_sse( + State(state): State, + Path(id): Path, +) -> Sse>> { + let stream = async_stream::stream! { + let mut last_id: i64 = 0; + loop { + tokio::time::sleep(Duration::from_secs(2)).await; + let db = state.db.lock().await; + let logs = db.get_logs_after(&id, last_id).unwrap_or_default(); + drop(db); + for log in logs { + last_id = log.id; + let data = serde_json::to_string(&log).unwrap_or_default(); + yield Ok(Event::default().event("log").data(data)); + } + } + }; + Sse::new(stream).keep_alive(KeepAlive::default()) +} diff --git a/src/api/routes.rs b/src/api/routes.rs index f2c7595..5ed21f5 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,31 +1,95 @@ use axum::{ + extract::State, middleware, - routing::{delete, get, post}, + response::{IntoResponse, Redirect}, + routing::{delete, get, post, put}, Router, }; +use axum_extra::extract::CookieJar; -use crate::api::{auth::require_auth, bots, events, mode, positions}; +use crate::api::{auth, bots, events, mode, positions}; use crate::AppState; pub fn build(state: AppState) -> Router { - let api = Router::new() + let protected_api = Router::new() .route("/symbols", get(bots::list_symbols)) .route("/bots", get(bots::list_bots).post(bots::create_bot)) - .route("/bots/:id", delete(bots::delete_bot)) + .route("/bots/:id", get(bots::get_bot).delete(bots::delete_bot).put(bots::update_bot)) .route("/bots/:id/start", post(bots::start_bot)) .route("/bots/:id/stop", post(bots::stop_bot)) + .route("/bots/:id/logs", get(bots::get_bot_logs)) + .route("/bots/:id/positions", get(bots::get_bot_positions)) + .route("/bots/:id/positions/closed", get(bots::get_bot_closed)) + .route("/bots/:id/log-stream", get(events::bot_log_sse)) .route("/positions", get(positions::open_positions)) .route("/positions/closed", get(positions::closed_positions)) .route("/mode", get(mode::get_mode).post(mode::set_mode)) .route("/events", get(events::sse_handler)) - .layer(middleware::from_fn_with_state(state.clone(), require_auth)); + .layer(middleware::from_fn_with_state(state.clone(), auth::require_auth)); + + let api = Router::new() + .route("/auth/login", post(auth::login)) + .route("/auth/logout", post(auth::logout)) + .merge(protected_api); Router::new() .nest("/api", api) - .route("/", get(index_handler)) + .route("/", get(login_handler)) + .route("/dashboard", get(dashboard_handler)) + .route("/bots/:id", get(bot_detail_handler)) .with_state(state) } -async fn index_handler() -> axum::response::Html<&'static str> { - axum::response::Html(include_str!("../web/index.html")) +async fn login_handler( + State(state): State, + jar: CookieJar, +) -> impl IntoResponse { + let token = jar.get("mse_session").map(|c| c.value().to_string()); + let authed = match token { + Some(t) => { + let db = state.db.lock().await; + db.session_exists(&t).unwrap_or(false) + } + None => false, + }; + if authed { + Redirect::to("/dashboard").into_response() + } else { + axum::response::Html(include_str!("../web/login.html")).into_response() + } +} + +async fn bot_detail_handler( + State(state): State, + jar: CookieJar, +) -> impl IntoResponse { + let token = jar.get("mse_session").map(|c| c.value().to_string()); + let authed = match token { + Some(t) => { let db = state.db.lock().await; db.session_exists(&t).unwrap_or(false) } + None => false, + }; + if authed { + axum::response::Html(include_str!("../web/bot.html")).into_response() + } else { + Redirect::to("/").into_response() + } +} + +async fn dashboard_handler( + State(state): State, + jar: CookieJar, +) -> impl IntoResponse { + let token = jar.get("mse_session").map(|c| c.value().to_string()); + let authed = match token { + Some(t) => { + let db = state.db.lock().await; + db.session_exists(&t).unwrap_or(false) + } + None => false, + }; + if authed { + axum::response::Html(include_str!("../web/index.html")).into_response() + } else { + Redirect::to("/").into_response() + } } diff --git a/src/bot/runner.rs b/src/bot/runner.rs index 3994a59..6fdfa6c 100644 --- a/src/bot/runner.rs +++ b/src/bot/runner.rs @@ -56,6 +56,10 @@ impl BotRunner { match connect_async(&stream).await { Ok((ws, _)) => { info!("[{}] WS bağlantısı kuruldu.", config.symbol); + { + let db_guard = db.lock().await; + db_guard.insert_log(&config.id, "info", "WS bağlantısı kuruldu.").ok(); + } let (_, mut read) = ws.split(); loop { @@ -82,6 +86,10 @@ impl BotRunner { } info!("[{}] Mum kapandı. Açılış: {:.6} | Kapanış: {:.6}", config.symbol, kline.open, kline.close); + { + let db_guard = db.lock().await; + db_guard.insert_log(&config.id, "info", &format!("Mum kapandı. Açılış: {:.6} | Kapanış: {:.6}", kline.open, kline.close)).ok(); + } let kline_data = Kline::from(&kline); match RedCandleStrategy::execute( @@ -98,6 +106,10 @@ impl BotRunner { "[{}] ✅ İşlem | Alış: {:.6} | Satış hedefi: {:.6} | Kar: %{:.2}", config.symbol, result.buy_order.price, result.sell_order.price, config.profit_percent ); + { + let db_guard = db.lock().await; + db_guard.insert_log(&config.id, "trade", &format!("✅ İşlem gerçekleşti | Alış: {:.6} | Satış hedefi: {:.6} | Miktar: {:.6} | Kar: %{:.2}", result.buy_order.price, result.sell_order.price, result.buy_order.quantity, config.profit_percent)).ok(); + } let record = TradeRecord { bot_id: config.id.clone(), @@ -144,10 +156,18 @@ impl BotRunner { }).ok(); } Ok(None) => { - info!("[{}] ⏭ İşlem yapılmadı.", config.symbol); + info!("[{}] ⏭ İşlem yapılmadı (yeşil mum).", config.symbol); + { + let db_guard = db.lock().await; + db_guard.insert_log(&config.id, "info", "⏭ Yeşil mum, işlem yapılmadı.").ok(); + } } Err(e) => { error!("[{}] ❌ Strateji hatası: {:?}", config.symbol, e); + { + let db_guard = db.lock().await; + db_guard.insert_log(&config.id, "error", &format!("❌ Strateji hatası: {}", e)).ok(); + } } } } @@ -164,6 +184,10 @@ impl BotRunner { } Err(e) => { error!("[{}] WS bağlanamadı: {}. 5sn sonra tekrar deneniyor.", config.symbol, e); + { + let db_guard = db.lock().await; + db_guard.insert_log(&config.id, "error", &format!("WS bağlanamadı: {}. 5sn sonra tekrar deneniyor.", e)).ok(); + } } } diff --git a/src/config.rs b/src/config.rs index 35eed00..4c5f5b9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ pub struct AppConfig { pub live_api_secret: String, pub testnet_api_key: String, pub testnet_api_secret: String, - pub auth_token: String, + pub password_hash: String, pub db_path: String, pub listen_addr: String, } @@ -18,7 +18,7 @@ impl AppConfig { live_api_secret: env::var("BINANCE_API_SECRET").expect("BINANCE_API_SECRET gerekli"), testnet_api_key: env::var("BINANCE_TESTNET_API_KEY").expect("BINANCE_TESTNET_API_KEY gerekli"), testnet_api_secret: env::var("BINANCE_TESTNET_API_SECRET").expect("BINANCE_TESTNET_API_SECRET gerekli"), - auth_token: env::var("AUTH_TOKEN").expect("AUTH_TOKEN gerekli"), + password_hash: env::var("ADMIN_PASSWORD_HASH").expect("ADMIN_PASSWORD_HASH gerekli"), db_path: env::var("DB_PATH").unwrap_or_else(|_| "data/bots.db".to_string()), listen_addr: env::var("LISTEN_ADDR").unwrap_or_else(|_| "127.0.0.1:4646".to_string()), } diff --git a/src/main.rs b/src/main.rs index 88bde82..4f0b4f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ pub struct AppState { pub db: Arc>, pub manager: Arc>, pub event_tx: broadcast::Sender, - pub auth_token: String, + pub password_hash: String, pub live_api_key: String, pub live_api_secret: String, pub testnet_api_key: String, @@ -79,7 +79,7 @@ async fn main() { db, manager, event_tx, - auth_token: cfg.auth_token, + password_hash: cfg.password_hash, live_api_key: cfg.live_api_key, live_api_secret: cfg.live_api_secret, testnet_api_key: cfg.testnet_api_key, diff --git a/src/storage/db.rs b/src/storage/db.rs index 62787a0..e4e9b75 100644 --- a/src/storage/db.rs +++ b/src/storage/db.rs @@ -5,6 +5,7 @@ use rusqlite::{params, Connection, Result as SqlResult}; use crate::storage::closed::ClosedPosition; use crate::storage::config::BotConfig; use crate::storage::history::TradeRecord; +use crate::storage::logs::BotLog; use crate::storage::positions::OpenPosition; pub struct Database { @@ -70,7 +71,19 @@ impl Database { CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT NOT NULL - );", + ); + CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + created_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS bot_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bot_id TEXT NOT NULL, + level TEXT NOT NULL, + message TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_bot_logs_bot_id ON bot_logs(bot_id, created_at DESC);", )?; Ok(Self { conn }) } @@ -118,6 +131,14 @@ impl Database { Ok(()) } + pub fn update_bot(&self, b: &BotConfig) -> SqlResult<()> { + self.conn.execute( + "UPDATE bots SET name = ?1, usdt_amount = ?2, profit_percent = ?3 WHERE id = ?4", + params![b.name, b.usdt_amount, b.profit_percent, b.id], + )?; + Ok(()) + } + pub fn set_bot_active(&self, id: &str, active: bool) -> SqlResult<()> { self.conn.execute( "UPDATE bots SET active = ?1 WHERE id = ?2", @@ -240,6 +261,127 @@ impl Database { Ok(()) } + // ─── Sessions ──────────────────────────────────── + + pub fn insert_session(&self, token: &str) -> SqlResult<()> { + let now = chrono::Utc::now().timestamp(); + self.conn.execute( + "INSERT INTO sessions (token, created_at) VALUES (?1, ?2)", + params![token, now], + )?; + Ok(()) + } + + pub fn session_exists(&self, token: &str) -> SqlResult { + let count: i64 = self.conn.query_row( + "SELECT COUNT(*) FROM sessions WHERE token = ?1", + params![token], + |row| row.get(0), + )?; + Ok(count > 0) + } + + pub fn delete_session(&self, token: &str) -> SqlResult<()> { + self.conn.execute("DELETE FROM sessions WHERE token = ?1", params![token])?; + Ok(()) + } + + // ─── Bot logs ───────────────────────────────────── + + pub fn insert_log(&self, bot_id: &str, level: &str, message: &str) -> SqlResult<()> { + let now = chrono::Utc::now().timestamp_millis(); + self.conn.execute( + "INSERT INTO bot_logs (bot_id, level, message, created_at) VALUES (?1, ?2, ?3, ?4)", + params![bot_id, level, message, now], + )?; + // Son 500 logu tut + self.conn.execute( + "DELETE FROM bot_logs WHERE bot_id = ?1 AND id NOT IN (SELECT id FROM bot_logs WHERE bot_id = ?1 ORDER BY id DESC LIMIT 500)", + params![bot_id, bot_id], + )?; + Ok(()) + } + + pub fn get_logs(&self, bot_id: &str, limit: usize) -> SqlResult> { + let mut stmt = self.conn.prepare( + "SELECT id, bot_id, level, message, created_at FROM bot_logs WHERE bot_id = ?1 ORDER BY id DESC LIMIT ?2", + )?; + let rows = stmt.query_map(params![bot_id, limit as i64], |row| { + Ok(BotLog { + id: row.get(0)?, + bot_id: row.get(1)?, + level: row.get(2)?, + message: row.get(3)?, + created_at: row.get(4)?, + }) + })?; + let mut logs: Vec = rows.collect::>()?; + logs.reverse(); + Ok(logs) + } + + pub fn get_logs_after(&self, bot_id: &str, after_id: i64) -> SqlResult> { + let mut stmt = self.conn.prepare( + "SELECT id, bot_id, level, message, created_at FROM bot_logs WHERE bot_id = ?1 AND id > ?2 ORDER BY id ASC", + )?; + let rows = stmt.query_map(params![bot_id, after_id], |row| { + Ok(BotLog { + id: row.get(0)?, + bot_id: row.get(1)?, + level: row.get(2)?, + message: row.get(3)?, + created_at: row.get(4)?, + }) + })?; + rows.collect() + } + + pub fn get_positions_by_bot(&self, bot_id: &str) -> SqlResult> { + let mut stmt = self.conn.prepare( + "SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at + FROM open_positions WHERE bot_id = ?1 ORDER BY opened_at DESC", + )?; + let rows = stmt.query_map(params![bot_id], |row| { + let order_id: i64 = row.get(0)?; + Ok(crate::storage::positions::OpenPosition { + order_id: order_id as u64, + bot_id: row.get(1)?, + bot_name: row.get(2)?, + symbol: row.get(3)?, + buy_price: row.get(4)?, + sell_target: row.get(5)?, + quantity: row.get(6)?, + profit_percent: row.get(7)?, + opened_at: row.get(8)?, + }) + })?; + rows.collect() + } + + pub fn get_closed_by_bot(&self, bot_id: &str) -> SqlResult> { + let mut stmt = self.conn.prepare( + "SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at, closed_at, status + FROM closed_positions WHERE bot_id = ?1 ORDER BY closed_at DESC LIMIT 100", + )?; + let rows = stmt.query_map(params![bot_id], |row| { + let order_id: i64 = row.get(0)?; + Ok(crate::storage::closed::ClosedPosition { + order_id: order_id as u64, + bot_id: row.get(1)?, + bot_name: row.get(2)?, + symbol: row.get(3)?, + buy_price: row.get(4)?, + sell_target: row.get(5)?, + quantity: row.get(6)?, + profit_percent: row.get(7)?, + opened_at: row.get(8)?, + closed_at: row.get(9)?, + status: row.get(10)?, + }) + })?; + rows.collect() + } + // ─── Closed positions ──────────────────────────── pub fn get_closed_positions(&self) -> SqlResult> { diff --git a/src/storage/logs.rs b/src/storage/logs.rs new file mode 100644 index 0000000..774db2f --- /dev/null +++ b/src/storage/logs.rs @@ -0,0 +1,10 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct BotLog { + pub id: i64, + pub bot_id: String, + pub level: String, + pub message: String, + pub created_at: i64, +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 84b660b..4b0d1d6 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2,4 +2,5 @@ pub mod closed; pub mod config; pub mod db; pub mod history; +pub mod logs; pub mod positions; diff --git a/src/web/bot.html b/src/web/bot.html new file mode 100644 index 0000000..dcbecaa --- /dev/null +++ b/src/web/bot.html @@ -0,0 +1,625 @@ + + + + + + Bot Detay — CryptoFox Mukan Edition + + + + + + + +
+ +
+
+
+
+
+
+
+
24s Yüksek
+
24s Düşük
+
24s Hacim
+
+ +
+
+ ← Dashboard + +
+ + +
+ + +
+ + +
+
+ Bot Ayarları + ✓ Kaydedildi +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
Pozisyonlar
+
+
Açık
+
Kapalı
+
+
+
Yükleniyor...
+
+
+ +
+ + +
+
+ +
+ A: + Y: + D: + K: +
+
+
+
+
+ + +
+
+ Canlı Log +
+ + + +
+
+
+
+
+ +
+ + + + diff --git a/src/web/index.html b/src/web/index.html index 443b2a7..2bda1d5 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -56,14 +56,6 @@ .btn-primary:hover { background: #5a52d5; } .empty { padding: 24px 18px; color: var(--muted); font-size: 13px; text-align: center; } - /* Auth overlay */ - #auth-overlay { position: fixed; inset: 0; background: var(--bg); display: flex; align-items: center; justify-content: center; z-index: 200; } - .auth-box { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 32px; width: 320px; display: flex; flex-direction: column; gap: 16px; } - .auth-box h2 { font-size: 16px; color: var(--accent); } - .auth-box input { background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: 9px 12px; color: var(--text); font-size: 14px; width: 100%; } - .auth-box input:focus { outline: none; border-color: var(--accent); } - .auth-error { color: var(--red); font-size: 12px; display: none; } - /* Modal */ #modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: none; align-items: center; justify-content: center; z-index: 100; } #modal-overlay.open { display: flex; } @@ -102,14 +94,6 @@ -
-
-

Mukan Special Edition

- - - Geçersiz token -
-