added config file support
This commit is contained in:
Generated
+175
-2
@@ -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"
|
||||
|
||||
+2
-1
@@ -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"
|
||||
image = "0.24"
|
||||
config = "0.13"
|
||||
+20
@@ -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
|
||||
}
|
||||
}
|
||||
+109
-24
@@ -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<RwLock<HashMap<String, mpsc::UnboundedSender<Message>>>>;
|
||||
|
||||
// Структура для конфигурации
|
||||
#[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<u32>,
|
||||
}
|
||||
|
||||
#[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<Self, Box<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
// 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<dyn std::error::Error>> {
|
||||
.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::<SocketAddr>()?;
|
||||
|
||||
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<SqlitePool, Box<dyn std::error::Error>> {
|
||||
let pool = SqlitePool::connect("sqlite:chat.db").await?;
|
||||
async fn init_db(database_url: &str) -> Result<SqlitePool, Box<dyn std::error::Error>> {
|
||||
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<Arc<AppState>>,
|
||||
Json(payload): Json<ValidateTokenRequest>,
|
||||
@@ -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 {
|
||||
|
||||
+338
-114
@@ -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 @@
|
||||
<div class="mobile-header">
|
||||
<button class="menu-btn" onclick="toggleChatsPanel()">☰</button>
|
||||
<div class="mobile-title" id="mobileChatTitle">Secure Chat</div>
|
||||
<button class="theme-toggle" onclick="toggleTheme()">🌓</button>
|
||||
<button class="profile-header-btn" onclick="openProfileModal()">
|
||||
<div class="profile-header-avatar" id="profileHeaderAvatar">
|
||||
<!-- Avatar will be populated here -->
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="desktop-layout">
|
||||
@@ -1134,7 +1224,11 @@
|
||||
<div class="chats-panel" id="chatsPanel">
|
||||
<div class="chats-header">
|
||||
<h2>Chats</h2>
|
||||
<button class="theme-toggle" onclick="toggleTheme()">🌓</button>
|
||||
<button class="profile-header-btn" onclick="openProfileModal()">
|
||||
<div class="profile-header-avatar" id="profileHeaderAvatarDesktop">
|
||||
<!-- Avatar will be populated here -->
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="chats-tabs">
|
||||
@@ -1167,11 +1261,7 @@
|
||||
<div class="chat-header-phone" id="currentUserPhone">Choose someone to message</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<input type="file" id="avatarInput" class="avatar-input" accept="image/*">
|
||||
<button class="avatar-upload-btn" onclick="document.getElementById('avatarInput').click()">Upload Avatar</button>
|
||||
<button class="logout-btn" onclick="logout()">Logout</button>
|
||||
</div>
|
||||
<!-- Убрана кнопка профиля из заголовка чата -->
|
||||
</div>
|
||||
|
||||
<div class="chat-messages-container">
|
||||
@@ -1193,6 +1283,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Modal -->
|
||||
<div id="profileModal" class="profile-modal-overlay" style="display: none;">
|
||||
<div class="profile-modal">
|
||||
<div class="profile-header">
|
||||
<input type="file" id="avatarInput" class="avatar-input" accept="image/*">
|
||||
<div class="profile-avatar-large" id="profileModalAvatar" onclick="document.getElementById('avatarInput').click()">
|
||||
<!-- Large avatar will be populated here -->
|
||||
<div class="avatar-upload-overlay">
|
||||
Change Photo
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-name" id="profileModalName">User</div>
|
||||
<div class="profile-phone" id="profileModalPhone">+1234567890</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-content">
|
||||
<div class="profile-section">
|
||||
<div class="profile-section-title">
|
||||
📱 Account Information
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Username</div>
|
||||
<div class="info-value" id="profileModalUsername">user123</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Phone Number</div>
|
||||
<div class="info-value" id="profileModalPhoneNumber">+1234567890</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value">Online</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<div class="profile-section-title">
|
||||
⚙️ Settings
|
||||
</div>
|
||||
<div class="profile-actions">
|
||||
<button class="profile-btn profile-btn-secondary" onclick="toggleTheme()">
|
||||
🌓 Switch Theme
|
||||
</button>
|
||||
<button class="profile-btn profile-btn-primary" onclick="changePhoneNumber()">
|
||||
📞 Change Phone Number
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<div class="profile-actions">
|
||||
<button class="profile-btn profile-btn-danger" onclick="logout()">
|
||||
🚪 Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for delete confirmation -->
|
||||
<div id="deleteModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal">
|
||||
@@ -1212,6 +1363,7 @@
|
||||
let currentToken = '';
|
||||
let currentRecipient = '';
|
||||
let currentUserAvatar = '';
|
||||
let currentUserPhone = '';
|
||||
let chats = [];
|
||||
let searchResults = [];
|
||||
let messageHistory = {};
|
||||
@@ -1231,8 +1383,10 @@
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const savedToken = localStorage.getItem('chatToken');
|
||||
const savedUsername = localStorage.getItem('chatUsername');
|
||||
const savedPhone = localStorage.getItem('chatUserPhone');
|
||||
|
||||
if (savedToken && savedUsername) {
|
||||
currentUserPhone = savedPhone;
|
||||
validateToken(savedToken, savedUsername);
|
||||
}
|
||||
|
||||
@@ -1272,6 +1426,70 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openProfileModal() {
|
||||
updateProfileModal();
|
||||
document.getElementById('profileModal').style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
function closeProfileModal() {
|
||||
document.getElementById('profileModal').style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
|
||||
function updateProfileModal() {
|
||||
// Update profile modal content
|
||||
document.getElementById('profileModalName').textContent = currentUser;
|
||||
document.getElementById('profileModalUsername').textContent = currentUser;
|
||||
document.getElementById('profileModalPhone').textContent = currentUserPhone;
|
||||
document.getElementById('profileModalPhoneNumber').textContent = currentUserPhone;
|
||||
|
||||
// Update large avatar in modal
|
||||
const modalAvatar = document.getElementById('profileModalAvatar');
|
||||
if (currentUserAvatar) {
|
||||
modalAvatar.innerHTML = `<img src="${currentUserAvatar}" alt="${currentUser}">`;
|
||||
} else {
|
||||
modalAvatar.innerHTML = currentUser.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
// Add the upload overlay
|
||||
modalAvatar.innerHTML += '<div class="avatar-upload-overlay">Change Photo</div>';
|
||||
}
|
||||
|
||||
function updateAllAvatars() {
|
||||
const avatarContent = currentUserAvatar ?
|
||||
`<img src="${currentUserAvatar}" alt="${currentUser}">` :
|
||||
currentUser.charAt(0).toUpperCase();
|
||||
|
||||
// Update all avatar locations (убран profileHeaderAvatarMain)
|
||||
const avatarLocations = [
|
||||
'profileHeaderAvatar',
|
||||
'profileHeaderAvatarDesktop',
|
||||
'currentUserAvatar'
|
||||
];
|
||||
|
||||
avatarLocations.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.innerHTML = avatarContent;
|
||||
}
|
||||
});
|
||||
|
||||
// Update profile modal avatar
|
||||
updateProfileModal();
|
||||
}
|
||||
|
||||
function changePhoneNumber() {
|
||||
const newPhone = prompt('Enter your new phone number:', currentUserPhone);
|
||||
if (newPhone && newPhone.trim() !== '') {
|
||||
// In a real app, you would send this to the server
|
||||
currentUserPhone = newPhone.trim();
|
||||
localStorage.setItem('chatUserPhone', currentUserPhone);
|
||||
updateProfileModal();
|
||||
showAlert('Phone number updated successfully!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function showTab(tabName) {
|
||||
document.querySelectorAll('.auth-tab').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.auth-form').forEach(form => form.classList.remove('active'));
|
||||
@@ -1379,13 +1597,7 @@
|
||||
|
||||
function updateUserProfile() {
|
||||
document.getElementById('currentUsername').textContent = currentUser;
|
||||
|
||||
const avatarElement = document.getElementById('currentUserAvatar');
|
||||
if (currentUserAvatar) {
|
||||
avatarElement.innerHTML = `<img src="${currentUserAvatar}" alt="${currentUser}">`;
|
||||
} else {
|
||||
avatarElement.innerHTML = currentUser.charAt(0).toUpperCase();
|
||||
}
|
||||
updateAllAvatars();
|
||||
}
|
||||
|
||||
async function loadChats() {
|
||||
@@ -1498,7 +1710,7 @@
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
currentUserAvatar = data.avatar_url;
|
||||
updateUserProfile();
|
||||
updateAllAvatars();
|
||||
showAlert('Avatar uploaded successfully!', 'success');
|
||||
loadChats(); // Refresh chats to show updated avatar
|
||||
} else {
|
||||
@@ -1704,9 +1916,11 @@
|
||||
if (data.success) {
|
||||
localStorage.setItem('chatToken', data.token);
|
||||
localStorage.setItem('chatUsername', data.username);
|
||||
localStorage.setItem('chatUserPhone', phone);
|
||||
|
||||
currentUser = data.username;
|
||||
currentToken = data.token;
|
||||
currentUserPhone = phone;
|
||||
|
||||
document.getElementById('authContainer').style.display = 'none';
|
||||
document.getElementById('chatContainer').style.display = 'flex';
|
||||
@@ -1745,9 +1959,11 @@
|
||||
if (data.success) {
|
||||
localStorage.setItem('chatToken', data.token);
|
||||
localStorage.setItem('chatUsername', data.username);
|
||||
localStorage.setItem('chatUserPhone', phone);
|
||||
|
||||
currentUser = data.username;
|
||||
currentToken = data.token;
|
||||
currentUserPhone = phone;
|
||||
|
||||
// Get user profile to load avatar
|
||||
const profileResponse = await fetch('/api/profile', {
|
||||
@@ -1776,6 +1992,7 @@
|
||||
function logout() {
|
||||
localStorage.removeItem('chatToken');
|
||||
localStorage.removeItem('chatUsername');
|
||||
localStorage.removeItem('chatUserPhone');
|
||||
|
||||
if (ws) {
|
||||
ws.close();
|
||||
@@ -1788,6 +2005,7 @@
|
||||
currentToken = '';
|
||||
currentRecipient = '';
|
||||
currentUserAvatar = '';
|
||||
currentUserPhone = '';
|
||||
chats = [];
|
||||
searchResults = [];
|
||||
messageHistory = {};
|
||||
@@ -2147,7 +2365,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
// Close modals when clicking outside
|
||||
document.getElementById('profileModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeProfileModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('deleteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeDeleteModal();
|
||||
|
||||
Reference in New Issue
Block a user