added config file support

This commit is contained in:
wt
2025-11-23 21:29:49 +07:00
parent ded35ae796
commit 88162a9245
5 changed files with 644 additions and 141 deletions
Generated
+175 -2
View File
@@ -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"
+1
View File
@@ -18,3 +18,4 @@ rand = "0.8"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4"] }
image = "0.24"
config = "0.13"
+20
View File
@@ -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
View File
@@ -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 {
+331 -107
View File
@@ -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;
.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);
font-size: 14px;
.profile-info {
background: var(--bg-secondary);
border-radius: 10px;
padding: 15px;
}
.modal-buttons {
.info-item {
display: flex;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.modal-btn {
padding: 8px 16px;
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-size: 14px;
color: var(--text-secondary);
}
.info-value {
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
}
.profile-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.profile-btn {
padding: 12px 16px;
border: none;
border-radius: 5px;
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 {
background: var(--bg-tertiary);
color: var(--text-primary);
.profile-btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
}
.modal-btn-cancel:hover {
background: var(--border-color);
.profile-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.modal-btn-delete {
.profile-btn-danger {
background: var(--error-color);
color: white;
}
.modal-btn-delete:hover {
.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 {
background: var(--primary-color);
color: white;
.profile-btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.modal-btn-delete-for-me:hover {
background: var(--primary-hover);
.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();