diff --git a/Cargo.lock b/Cargo.lock index 8336e25..503e423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -127,6 +138,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -268,6 +285,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -413,6 +449,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dotenvy" version = "0.15.7" @@ -695,13 +737,22 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "allocator-api2", ] @@ -1039,6 +1090,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1088,6 +1150,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1187,6 +1255,7 @@ dependencies = [ "axum", "bcrypt", "chrono", + "config", "futures-util", "image", "rand", @@ -1349,6 +1418,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1378,6 +1457,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1393,6 +1478,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1555,6 +1683,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "rsa" version = "0.9.9" @@ -1575,6 +1714,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustix" version = "1.1.2" @@ -1836,7 +1985,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ - "ahash", + "ahash 0.8.12", "atoi", "byteorder", "bytes", @@ -2220,6 +2369,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tower" version = "0.5.2" @@ -2355,6 +2513,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicase" version = "2.8.1" @@ -2760,6 +2924,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index fe91289..d21e70a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ bcrypt = "0.15" rand = "0.8" chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.0", features = ["v4"] } -image = "0.24" \ No newline at end of file +image = "0.24" +config = "0.13" \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..9baf180 --- /dev/null +++ b/config.json @@ -0,0 +1,20 @@ +{ + "server": { + "host": "0.0.0.0", + "port": 3333, + "max_connections": 10000 + }, + "database": { + "url": "sqlite:chat.db" + }, + "files": { + "avatars_dir": "web/avatars", + "web_dir": "web", + "max_avatar_size": 5242880 + }, + "limits": { + "max_message_length": 4096, + "max_username_length": 50, + "min_password_length": 6 + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c209d09..f1af9ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,13 +7,13 @@ use axum::{ routing::{get, post, delete}, Router, }; +use config::{Config as ConfigLoader, File, FileFormat}; use serde::{Deserialize, Serialize}; use sqlx::{sqlite::SqlitePool, Row}; use std::{ collections::HashMap, net::SocketAddr, sync::Arc, - path::Path, }; use tokio::sync::{mpsc, RwLock}; use tower_http::services::ServeDir; @@ -28,10 +28,80 @@ use std::io::Cursor; // Types for shared state type UserMap = Arc>>>; +// Структура для конфигурации +#[derive(Debug, Deserialize, Clone)] +struct AppConfig { + server: ServerConfig, + database: DatabaseConfig, + files: FilesConfig, + limits: LimitsConfig, +} + +#[derive(Debug, Deserialize, Clone)] +struct ServerConfig { + host: String, + port: u16, + max_connections: Option, +} + +#[derive(Debug, Deserialize, Clone)] +struct DatabaseConfig { + url: String, +} + +#[derive(Debug, Deserialize, Clone)] +struct FilesConfig { + avatars_dir: String, + web_dir: String, + max_avatar_size: usize, +} + +#[derive(Debug, Deserialize, Clone)] +struct LimitsConfig { + max_message_length: usize, + max_username_length: usize, + min_password_length: usize, +} + +impl AppConfig { + fn load() -> Result> { + let config_loader = ConfigLoader::builder() + // Добавляем конфиг по умолчанию + .add_source(File::from_str( + r#" + [server] + host = "0.0.0.0" + port = 3333 + max_connections = 10000 + + [database] + url = "sqlite:chat.db" + + [files] + avatars_dir = "web/avatars" + web_dir = "web" + max_avatar_size = 5242880 + + [limits] + max_message_length = 4096 + max_username_length = 50 + min_password_length = 6 + "#, + FileFormat::Toml, + )) + // Перезаписываем значения из config.json если он существует + .add_source(File::with_name("config").required(false)) + .build()?; + + Ok(config_loader.try_deserialize()?) + } +} + #[derive(Clone)] struct AppState { users: UserMap, db_pool: SqlitePool, + config: AppConfig, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -127,21 +197,32 @@ struct UploadAvatarResponse { message: String, } +#[derive(Debug, Deserialize)] +struct ValidateTokenRequest { + token: String, +} + #[tokio::main] async fn main() -> Result<(), Box> { // Initialize logging tracing_subscriber::fmt::init(); - // Create avatars directory if it doesn't exist - tokio::fs::create_dir_all("web/avatars").await?; + // Load configuration + let config = AppConfig::load()?; + println!("📋 Loaded configuration: {:?}", config); - // Initialize database - let db_pool = init_db().await?; + // Create directories if they don't exist + tokio::fs::create_dir_all(&config.files.avatars_dir).await?; + tokio::fs::create_dir_all(&config.files.web_dir).await?; + + // Initialize database with configured URL + let db_pool = init_db(&config.database.url).await?; // Create shared state let state = Arc::new(AppState { users: Arc::new(RwLock::new(HashMap::new())), db_pool, + config, }); // Build the application @@ -157,20 +238,29 @@ async fn main() -> Result<(), Box> { .route("/api/delete-message", post(delete_message_handler)) .route("/api/upload-avatar", post(upload_avatar_handler)) .route("/api/profile", get(profile_handler)) - .nest_service("/", ServeDir::new("web")) - .with_state(state); + .nest_service("/", ServeDir::new(&state.config.files.web_dir)) + .with_state(state.clone()); - let addr = SocketAddr::from(([0, 0, 0, 0], 3333)); - println!("Server running on http://{}", addr); + let addr = format!("{}:{}", state.config.server.host, state.config.server.port) + .parse::()?; + + println!("🚀 Server running on http://{}", addr); + println!("📁 Web directory: {}", state.config.files.web_dir); + println!("💾 Database: {}", state.config.database.url); let listener = tokio::net::TcpListener::bind(addr).await?; + + if let Some(max_connections) = state.config.server.max_connections { + println!("🔒 Max connections: {}", max_connections); + } + axum::serve(listener, app).await?; Ok(()) } -async fn init_db() -> Result> { - let pool = SqlitePool::connect("sqlite:chat.db").await?; +async fn init_db(database_url: &str) -> Result> { + let pool = SqlitePool::connect(database_url).await?; // Create users table sqlx::query( @@ -241,19 +331,19 @@ async fn register_handler( }); } - if payload.username.len() < 3 { + if payload.username.len() < 3 || payload.username.len() > state.config.limits.max_username_length { return Json(AuthResponse { success: false, - message: "Username must be at least 3 characters".to_string(), + message: format!("Username must be between 3 and {} characters", state.config.limits.max_username_length), username: None, token: None, }); } - if payload.password.len() < 6 { + if payload.password.len() < state.config.limits.min_password_length { return Json(AuthResponse { success: false, - message: "Password must be at least 6 characters".to_string(), + message: format!("Password must be at least {} characters", state.config.limits.min_password_length), username: None, token: None, }); @@ -396,11 +486,6 @@ async fn login_handler( } } -#[derive(Debug, Deserialize)] -struct ValidateTokenRequest { - token: String, -} - async fn validate_token_handler( State(state): State>, Json(payload): Json, @@ -1000,12 +1085,12 @@ async fn upload_avatar_handler( if field_name == "avatar" { let data = field.bytes().await.unwrap(); - // Validate image size (max 5MB) - if data.len() > 5 * 1024 * 1024 { + // Validate image size using config + if data.len() > state.config.files.max_avatar_size { return Json(UploadAvatarResponse { success: false, avatar_url: None, - message: "Image too large (max 5MB)".to_string(), + message: format!("Image too large (max {} bytes)", state.config.files.max_avatar_size), }); } @@ -1014,7 +1099,7 @@ async fn upload_avatar_handler( Ok(processed_data) => { // Generate unique filename let filename = format!("{}.jpg", Uuid::new_v4()); - let avatar_path = format!("web/avatars/{}", filename); + let avatar_path = format!("{}/{}", state.config.files.avatars_dir, filename); // Save file if let Err(e) = tokio::fs::write(&avatar_path, &processed_data).await { diff --git a/web/index.html b/web/index.html index f14c94a..7dcd861 100644 --- a/web/index.html +++ b/web/index.html @@ -204,14 +204,38 @@ font-size: 18px; } - .theme-toggle { + .profile-header-btn { background: none; border: none; - color: var(--text-primary); - font-size: 18px; cursor: pointer; padding: 5px; - margin-left: 10px; + border-radius: 50%; + transition: background 0.3s ease; + } + + .profile-header-btn:hover { + background: var(--bg-tertiary); + } + + .profile-header-avatar { + width: 35px; + height: 35px; + border-radius: 50%; + object-fit: cover; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + font-size: 14px; + font-weight: 600; + } + + .profile-header-avatar img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; } .chats-panel { @@ -464,41 +488,6 @@ color: var(--text-secondary); } - .header-actions { - display: flex; - align-items: center; - gap: 8px; - } - - .avatar-upload-btn { - padding: 6px 10px; - background: var(--primary-color); - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 11px; - white-space: nowrap; - } - - .avatar-upload-btn:hover { - background: var(--primary-hover); - } - - .logout-btn { - padding: 6px 12px; - background: var(--error-color); - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 12px; - } - - .logout-btn:hover { - background: #c0392b; - } - .chat-messages { flex: 1; padding: 15px; @@ -699,8 +688,8 @@ color: var(--error-color); } - /* Стили для модального окна удаления */ - .modal-overlay { + /* Стили для модального окна профиля */ + .profile-modal-overlay { position: fixed; top: 0; left: 0; @@ -713,68 +702,186 @@ z-index: 1000; } - .modal { + .profile-modal { background: var(--bg-primary); - border-radius: 10px; - padding: 20px; - max-width: 350px; + border-radius: 15px; + padding: 0; + max-width: 400px; width: 90%; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); margin: 20px; + overflow: hidden; } - .modal h3 { - margin-bottom: 12px; - color: var(--text-primary); + .profile-header { + background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); + padding: 30px 20px; + text-align: center; + color: white; + position: relative; + } + + .profile-avatar-large { + width: 100px; + height: 100px; + border-radius: 50%; + margin: 0 auto 15px; + background: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 36px; + font-weight: 600; + border: 4px solid white; + position: relative; + cursor: pointer; + transition: transform 0.3s ease; + } + + .profile-avatar-large:hover { + transform: scale(1.05); + } + + .profile-avatar-large img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + } + + .avatar-upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + color: white; + font-size: 12px; + text-align: center; + padding: 10px; + } + + .profile-avatar-large:hover .avatar-upload-overlay { + opacity: 1; + } + + .profile-name { + font-size: 20px; + font-weight: 600; + margin-bottom: 5px; + } + + .profile-phone { + font-size: 14px; + opacity: 0.9; + } + + .profile-content { + padding: 25px; + } + + .profile-section { + margin-bottom: 25px; + } + + .profile-section-title { + font-size: 16px; + font-weight: 600; + margin-bottom: 15px; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 8px; + } + + .profile-section-title i { font-size: 18px; } - .modal p { - margin-bottom: 18px; - color: var(--text-secondary); + .profile-info { + background: var(--bg-secondary); + border-radius: 10px; + padding: 15px; + } + + .info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid var(--border-color); + } + + .info-item:last-child { + border-bottom: none; + } + + .info-label { font-size: 14px; + color: var(--text-secondary); } - .modal-buttons { - display: flex; - gap: 8px; - justify-content: flex-end; - flex-wrap: wrap; + .info-value { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; } - .modal-btn { - padding: 8px 16px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 14px; + .profile-actions { + display: flex; + flex-direction: column; + gap: 12px; } - .modal-btn-cancel { - background: var(--bg-tertiary); - color: var(--text-primary); + .profile-btn { + padding: 12px 16px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 10px; } - .modal-btn-cancel:hover { - background: var(--border-color); + .profile-btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); + color: white; } - .modal-btn-delete { - background: var(--error-color); - color: white; + .profile-btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4); } - .modal-btn-delete:hover { - background: #c0392b; + .profile-btn-danger { + background: var(--error-color); + color: white; } - .modal-btn-delete-for-me { - background: var(--primary-color); - color: white; + .profile-btn-danger:hover { + background: #c0392b; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(231, 76, 60, 0.4); } - .modal-btn-delete-for-me:hover { - background: var(--primary-hover); + .profile-btn-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + } + + .profile-btn-secondary:hover { + background: var(--border-color); } /* Стили для скрытого input file */ @@ -876,17 +983,6 @@ display: block; } - .header-actions { - flex-wrap: wrap; - justify-content: flex-end; - } - - .avatar-upload-btn, - .logout-btn { - font-size: 12px; - padding: 6px 8px; - } - .message { max-width: 90%; margin: 6px 0; @@ -1013,16 +1109,6 @@ padding: 10px 12px; } - .header-actions { - gap: 5px; - } - - .avatar-upload-btn, - .logout-btn { - font-size: 11px; - padding: 5px 6px; - } - .chat-input-area { gap: 6px; } @@ -1126,7 +1212,11 @@
Secure Chat
- +
@@ -1134,7 +1224,11 @@

Chats

- +
@@ -1167,11 +1261,7 @@
Choose someone to message
-
- - - -
+
@@ -1193,6 +1283,67 @@
+ + +