more refactor on backend
This commit is contained in:
1 parent
941afa2485
commit
7f7936d37f
20 files changed
+1603
-1527
No files matched your search
@@ -0,0 +1,214 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
|
||||
// УПРОЩЕННАЯ версия получения чатов
|
||||
pub async fn get_chats_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Json<Vec<model::ChatPartner>> {
|
||||
// Extract token from header
|
||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
||||
auth_header.to_str().unwrap_or("").replace("Bearer ", "")
|
||||
} else {
|
||||
return Json(Vec::new());
|
||||
};
|
||||
|
||||
// Get current user from token
|
||||
let username = match utils::verify_token(&state.db_pool, &token).await {
|
||||
Ok(Some(user)) => user,
|
||||
_ => return Json(Vec::new()),
|
||||
};
|
||||
|
||||
println!("🔍 Fetching chats for user: {}", username);
|
||||
|
||||
// ПРОСТОЙ И ПРЯМОЙ ЗАПРОС: находим всех пользователей, с которыми есть сообщения
|
||||
let chat_partners = sqlx::query(
|
||||
r#"
|
||||
-- Находим всех уникальных собеседников
|
||||
SELECT DISTINCT
|
||||
CASE
|
||||
WHEN from_user = ? THEN to_user
|
||||
ELSE from_user
|
||||
END as partner_username
|
||||
FROM messages
|
||||
WHERE (from_user = ? OR to_user = ?)
|
||||
AND deleted_for_everyone = FALSE
|
||||
AND ((from_user = ? AND deleted_for_sender = FALSE) OR (to_user = ? AND deleted_for_receiver = FALSE))
|
||||
"#
|
||||
)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.fetch_all(&state.db_pool)
|
||||
.await;
|
||||
|
||||
let partner_usernames = match chat_partners {
|
||||
Ok(rows) => {
|
||||
let partners: Vec<String> = rows
|
||||
.iter()
|
||||
.map(|row| row.get::<String, _>("partner_username"))
|
||||
.collect();
|
||||
println!("🤝 Found partner usernames: {:?}", partners);
|
||||
partners
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error fetching partner usernames: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
if partner_usernames.is_empty() {
|
||||
println!("❌ No partners found for user {}", username);
|
||||
|
||||
// Проверим, есть ли вообще сообщения у пользователя
|
||||
let message_count = sqlx::query(
|
||||
"SELECT COUNT(*) as count FROM messages WHERE (from_user = ? OR to_user = ?) AND deleted_for_everyone = FALSE"
|
||||
)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.fetch_one(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match message_count {
|
||||
Ok(row) => {
|
||||
let count: i64 = row.get("count");
|
||||
println!(
|
||||
"📊 User {} has {} total messages in database",
|
||||
username, count
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error counting messages: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
return Json(Vec::new());
|
||||
}
|
||||
|
||||
// Теперь получаем детальную информацию о каждом собеседнике
|
||||
let mut chats = Vec::new();
|
||||
|
||||
for partner_username in partner_usernames {
|
||||
println!("📞 Getting details for partner: {}", partner_username);
|
||||
|
||||
// Получаем информацию о пользователе
|
||||
let user_result =
|
||||
sqlx::query("SELECT username, phone, avatar_url FROM users WHERE username = ?")
|
||||
.bind(&partner_username)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(Some(user_row)) => {
|
||||
let partner_name: String = user_row.get("username");
|
||||
let phone: String = user_row.get("phone");
|
||||
let avatar_url: Option<String> = user_row.get("avatar_url");
|
||||
|
||||
// Получаем последнее сообщение
|
||||
let last_message_result = sqlx::query(
|
||||
r#"
|
||||
SELECT content, timestamp
|
||||
FROM messages
|
||||
WHERE ((from_user = ? AND to_user = ?) OR (from_user = ? AND to_user = ?))
|
||||
AND deleted_for_everyone = FALSE
|
||||
AND ((from_user = ? AND deleted_for_sender = FALSE) OR (to_user = ? AND deleted_for_receiver = FALSE))
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
"#
|
||||
)
|
||||
.bind(&username) // текущий пользователь
|
||||
.bind(&partner_name) // собеседник
|
||||
.bind(&partner_name) // собеседник
|
||||
.bind(&username) // текущий пользователь
|
||||
.bind(&username) // для проверки deleted_for_sender
|
||||
.bind(&username) // для проверки deleted_for_receiver
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
let (last_message, last_message_time) = match last_message_result {
|
||||
Ok(Some(row)) => {
|
||||
let content: String = row.get("content");
|
||||
let timestamp: i64 = row.get("timestamp");
|
||||
println!(
|
||||
"💬 Last message with {}: '{}' at {}",
|
||||
partner_name, content, timestamp
|
||||
);
|
||||
(Some(content), Some(timestamp))
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("❌ No last message found for chat with {}", partner_name);
|
||||
(None, None)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error fetching last message: {}", e);
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
|
||||
// Получаем количество непрочитанных сообщений
|
||||
let unread_count_result = sqlx::query(
|
||||
"SELECT COUNT(*) as count FROM messages WHERE from_user = ? AND to_user = ? AND read = FALSE AND deleted_for_everyone = FALSE AND deleted_for_receiver = FALSE"
|
||||
)
|
||||
.bind(&partner_name)
|
||||
.bind(&username)
|
||||
.fetch_one(&state.db_pool)
|
||||
.await;
|
||||
|
||||
let unread_count = match unread_count_result {
|
||||
Ok(row) => row.get::<i64, _>("count"),
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error counting unread messages: {}", e);
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
let chat = model::ChatPartner {
|
||||
username: partner_name.clone(),
|
||||
phone,
|
||||
avatar_url,
|
||||
last_message,
|
||||
last_message_time,
|
||||
unread_count,
|
||||
};
|
||||
|
||||
println!(
|
||||
"✅ Added chat with {} (phone: {}), last_message: {:?}, unread: {}",
|
||||
chat.username, chat.phone, chat.last_message, chat.unread_count
|
||||
);
|
||||
chats.push(chat);
|
||||
}
|
||||
Ok(None) => {
|
||||
eprintln!("❌ User not found: {}", partner_username);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"❌ Error fetching user details for {}: {}",
|
||||
partner_username, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сортируем по времени последнего сообщения (новые сверху)
|
||||
chats.sort_by(|a, b| {
|
||||
b.last_message_time
|
||||
.unwrap_or(0)
|
||||
.cmp(&a.last_message_time.unwrap_or(0))
|
||||
});
|
||||
|
||||
println!("🎉 Final chat list for {}: {} chats", username, chats.len());
|
||||
for chat in &chats {
|
||||
println!(
|
||||
" - {}: {:?} (unread: {})",
|
||||
chat.username, chat.last_message, chat.unread_count
|
||||
);
|
||||
}
|
||||
|
||||
Json(chats)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Debug endpoint to see all messages
|
||||
pub async fn debug_messages_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Json<serde_json::Value> {
|
||||
// Extract token from header
|
||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
||||
auth_header.to_str().unwrap_or("").replace("Bearer ", "")
|
||||
} else {
|
||||
return Json(serde_json::json!({"error": "No token"}));
|
||||
};
|
||||
|
||||
// Get current user from token
|
||||
let username = match utils::verify_token(&state.db_pool, &token).await {
|
||||
Ok(Some(user)) => user,
|
||||
_ => return Json(serde_json::json!({"error": "Invalid token"})),
|
||||
};
|
||||
|
||||
// Get all messages for this user
|
||||
let messages = sqlx::query(
|
||||
r#"
|
||||
SELECT id, from_user, to_user, content, timestamp, message_type, read,
|
||||
deleted_for_sender, deleted_for_receiver, deleted_for_everyone
|
||||
FROM messages
|
||||
WHERE from_user = ? OR to_user = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100
|
||||
"#,
|
||||
)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.fetch_all(&state.db_pool)
|
||||
.await;
|
||||
|
||||
// Get chat partners count
|
||||
let partners_result = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(DISTINCT
|
||||
CASE
|
||||
WHEN from_user = ? THEN to_user
|
||||
ELSE from_user
|
||||
END
|
||||
) as partner_count
|
||||
FROM messages
|
||||
WHERE from_user = ? OR to_user = ?
|
||||
"#,
|
||||
)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.bind(&username)
|
||||
.fetch_one(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match messages {
|
||||
Ok(rows) => {
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
result.push(serde_json::json!({
|
||||
"id": row.get::<String, _>("id"),
|
||||
"from_user": row.get::<String, _>("from_user"),
|
||||
"to_user": row.get::<String, _>("to_user"),
|
||||
"content": row.get::<String, _>("content"),
|
||||
"timestamp": row.get::<i64, _>("timestamp"),
|
||||
"message_type": row.get::<String, _>("message_type"),
|
||||
"read": row.get::<bool, _>("read"),
|
||||
"deleted_for_sender": row.get::<bool, _>("deleted_for_sender"),
|
||||
"deleted_for_receiver": row.get::<bool, _>("deleted_for_receiver"),
|
||||
"deleted_for_everyone": row.get::<bool, _>("deleted_for_everyone"),
|
||||
"is_current_user_from": row.get::<String, _>("from_user") == username,
|
||||
"is_current_user_to": row.get::<String, _>("to_user") == username,
|
||||
}));
|
||||
}
|
||||
|
||||
let partner_count = match partners_result {
|
||||
Ok(row) => row.get::<i64, _>("partner_count"),
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
Json(serde_json::json!({
|
||||
"user": username,
|
||||
"total_messages": result.len(),
|
||||
"chat_partners_count": partner_count,
|
||||
"messages": result
|
||||
}))
|
||||
}
|
||||
Err(e) => Json(serde_json::json!({"error": format!("Database error: {}", e)})),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State};
|
||||
use bcrypt::verify;
|
||||
use chrono::{Duration, Utc};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn login_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
Json(payload): Json<model::LoginRequest>,
|
||||
) -> Json<model::AuthResponse> {
|
||||
// Get user from database
|
||||
let user_result = sqlx::query("SELECT username, password_hash FROM users WHERE phone = ?")
|
||||
.bind(&payload.phone)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(Some(row)) => {
|
||||
let username: String = row.get("username");
|
||||
let password_hash: String = row.get("password_hash");
|
||||
|
||||
// Verify password
|
||||
match verify(&payload.password, &password_hash) {
|
||||
Ok(true) => {
|
||||
// Generate session token
|
||||
let token = utils::generate_token();
|
||||
let expires_at = Utc::now() + Duration::days(30);
|
||||
|
||||
// Store session
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO sessions (token, username, phone, expires_at) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
.bind(&token)
|
||||
.bind(&username)
|
||||
.bind(&payload.phone)
|
||||
.bind(expires_at.naive_utc())
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
Json(model::AuthResponse {
|
||||
success: true,
|
||||
message: "Login successful".to_string(),
|
||||
username: Some(username),
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
Ok(false) => Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: "Invalid password".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
Err(_) => Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: "Error verifying password".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Ok(None) => Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: "Phone number not found".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
Err(_) => Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: "Login failed".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
pub mod delete;
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Mark messages as read endpoint
|
||||
pub async fn mark_read_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(payload): Json<model::MarkReadRequest>,
|
||||
) -> Json<serde_json::Value> {
|
||||
// Extract token from header
|
||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
||||
auth_header.to_str().unwrap_or("").replace("Bearer ", "")
|
||||
} else {
|
||||
return Json(serde_json::json!({"success": false, "error": "No token"}));
|
||||
};
|
||||
|
||||
// Get current user from token
|
||||
let username = match utils::verify_token(&state.db_pool, &token).await {
|
||||
Ok(Some(user)) => user,
|
||||
_ => return Json(serde_json::json!({"success": false, "error": "Invalid token"})),
|
||||
};
|
||||
|
||||
// Проверяем, что текущий пользователь - это получатель сообщений
|
||||
if username != payload.to_user {
|
||||
return Json(serde_json::json!({"success": false, "error": "Unauthorized"}));
|
||||
}
|
||||
|
||||
match utils::mark_messages_as_read(&state.db_pool, &payload.from_user, &payload.to_user).await {
|
||||
Ok(_) => Json(serde_json::json!({"success": true})),
|
||||
Err(e) => {
|
||||
eprintln!("Error marking messages as read: {}", e);
|
||||
Json(serde_json::json!({"success": false, "error": "Database error"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State, ws::Message};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Delete message endpoint
|
||||
pub async fn delete_message_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(payload): Json<model::DeleteMessageRequest>,
|
||||
) -> Json<model::DeleteMessageResponse> {
|
||||
// Extract token from header
|
||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
||||
auth_header.to_str().unwrap_or("").replace("Bearer ", "")
|
||||
} else {
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "No token".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
// Get current user from token
|
||||
let username = match utils::verify_token(&state.db_pool, &token).await {
|
||||
Ok(Some(user)) => user,
|
||||
_ => {
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Invalid token".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Получаем информацию о сообщении
|
||||
let message_result = sqlx::query("SELECT from_user, to_user FROM messages WHERE id = ?")
|
||||
.bind(&payload.message_id)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
let message = match message_result {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Message not found".to_string(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching message: {}", e);
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Database error".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let from_user: String = message.get("from_user");
|
||||
let to_user: String = message.get("to_user");
|
||||
|
||||
// Проверяем права доступа
|
||||
if from_user != username && to_user != username {
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "You don't have permission to delete this message".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if payload.for_everyone {
|
||||
// Удаление для всех
|
||||
if from_user != username {
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Only the sender can delete message for everyone".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let result = sqlx::query("UPDATE messages SET deleted_for_everyone = TRUE WHERE id = ?")
|
||||
.bind(&payload.message_id)
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// Отправляем уведомление через WebSocket
|
||||
let users = state.users.read().await;
|
||||
if let Some(recipient_tx) = users.get(&to_user) {
|
||||
let delete_msg = serde_json::json!({
|
||||
"type": "message_deleted",
|
||||
"message_id": payload.message_id,
|
||||
"for_everyone": true
|
||||
});
|
||||
let _ = recipient_tx.send(Message::Text(delete_msg.to_string()));
|
||||
}
|
||||
|
||||
Json(model::DeleteMessageResponse {
|
||||
success: true,
|
||||
message: "Message deleted for everyone".to_string(),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error deleting message for everyone: {}", e);
|
||||
Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Database error".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Удаление только для себя
|
||||
let field_to_update = if username == from_user {
|
||||
"deleted_for_sender"
|
||||
} else {
|
||||
"deleted_for_receiver"
|
||||
};
|
||||
|
||||
let query = format!(
|
||||
"UPDATE messages SET {} = TRUE WHERE id = ?",
|
||||
field_to_update
|
||||
);
|
||||
|
||||
let result = sqlx::query(&query)
|
||||
.bind(&payload.message_id)
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Json(model::DeleteMessageResponse {
|
||||
success: true,
|
||||
message: "Message deleted for you".to_string(),
|
||||
}),
|
||||
Err(e) => {
|
||||
eprintln!("Error deleting message for user: {}", e);
|
||||
Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Database error".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod avatar;
|
||||
pub mod get;
|
||||
@@ -0,0 +1,116 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, Multipart, State};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Upload avatar endpoint
|
||||
pub async fn upload_avatar_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
mut multipart: Multipart,
|
||||
) -> Json<model::UploadAvatarResponse> {
|
||||
// Extract token from header
|
||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
||||
auth_header.to_str().unwrap_or("").replace("Bearer ", "")
|
||||
} else {
|
||||
return Json(model::UploadAvatarResponse {
|
||||
success: false,
|
||||
avatar_url: None,
|
||||
message: "No token".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
// Get current user from token
|
||||
let username = match utils::verify_token(&state.db_pool, &token).await {
|
||||
Ok(Some(user)) => user,
|
||||
_ => {
|
||||
return Json(model::UploadAvatarResponse {
|
||||
success: false,
|
||||
avatar_url: None,
|
||||
message: "Invalid token".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Process multipart form
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
let field_name = field.name().unwrap_or("").to_string();
|
||||
|
||||
if field_name == "avatar" {
|
||||
let data = field.bytes().await.unwrap();
|
||||
|
||||
// Validate image size using config
|
||||
if data.len() > state.config.files.max_avatar_size {
|
||||
return Json(model::UploadAvatarResponse {
|
||||
success: false,
|
||||
avatar_url: None,
|
||||
message: format!(
|
||||
"Image too large (max {} bytes)",
|
||||
state.config.files.max_avatar_size
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Process image
|
||||
match utils::process_avatar(&data).await {
|
||||
Ok(processed_data) => {
|
||||
// Generate unique filename
|
||||
let filename = format!("{}.jpg", Uuid::new_v4());
|
||||
let avatar_path = format!("{}/{}", state.config.files.avatars_dir, filename);
|
||||
|
||||
// Save file
|
||||
if let Err(e) = tokio::fs::write(&avatar_path, &processed_data).await {
|
||||
eprintln!("Error saving avatar: {}", e);
|
||||
return Json(model::UploadAvatarResponse {
|
||||
success: false,
|
||||
avatar_url: None,
|
||||
message: "Error saving image".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let avatar_url = format!("/avatars/{}", filename);
|
||||
|
||||
// Update user in database
|
||||
let result = sqlx::query("UPDATE users SET avatar_url = ? WHERE username = ?")
|
||||
.bind(&avatar_url)
|
||||
.bind(&username)
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
return Json(model::UploadAvatarResponse {
|
||||
success: true,
|
||||
avatar_url: Some(avatar_url),
|
||||
message: "Avatar uploaded successfully".to_string(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error updating user avatar: {}", e);
|
||||
return Json(model::UploadAvatarResponse {
|
||||
success: false,
|
||||
avatar_url: None,
|
||||
message: "Database error".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Json(model::UploadAvatarResponse {
|
||||
success: false,
|
||||
avatar_url: None,
|
||||
message: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Json(model::UploadAvatarResponse {
|
||||
success: false,
|
||||
avatar_url: None,
|
||||
message: "No avatar file provided".to_string(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn profile_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Json<model::UserProfile> {
|
||||
// Extract token from header
|
||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
||||
auth_header.to_str().unwrap_or("").replace("Bearer ", "")
|
||||
} else {
|
||||
return Json(model::UserProfile {
|
||||
username: "user".to_string(),
|
||||
phone: "+1234567890".to_string(),
|
||||
avatar_url: None,
|
||||
});
|
||||
};
|
||||
|
||||
// Get current user from token
|
||||
let username = match utils::verify_token(&state.db_pool, &token).await {
|
||||
Ok(Some(user)) => user,
|
||||
_ => {
|
||||
return Json(model::UserProfile {
|
||||
username: "user".to_string(),
|
||||
phone: "+1234567890".to_string(),
|
||||
avatar_url: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get user details
|
||||
let user_result =
|
||||
sqlx::query("SELECT username, phone, avatar_url FROM users WHERE username = ?")
|
||||
.bind(&username)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(Some(row)) => Json(model::UserProfile {
|
||||
username: row.get("username"),
|
||||
phone: row.get("phone"),
|
||||
avatar_url: row.get("avatar_url"),
|
||||
}),
|
||||
_ => Json(model::UserProfile {
|
||||
username: "user".to_string(),
|
||||
phone: "+1234567890".to_string(),
|
||||
avatar_url: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use chrono::{Duration, Utc};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn register_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
Json(payload): Json<model::RegisterRequest>,
|
||||
) -> Json<model::AuthResponse> {
|
||||
// Validate input
|
||||
if payload.phone.len() < 5 {
|
||||
return Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: "Phone number is too short".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
if payload.username.len() < 3
|
||||
|| payload.username.len() > state.config.limits.max_username_length
|
||||
{
|
||||
return Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: format!(
|
||||
"Username must be between 3 and {} characters",
|
||||
state.config.limits.max_username_length
|
||||
),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
if payload.password.len() < state.config.limits.min_password_length {
|
||||
return Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: format!(
|
||||
"Password must be at least {} characters",
|
||||
state.config.limits.min_password_length
|
||||
),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
let password_hash = match hash(&payload.password, DEFAULT_COST) {
|
||||
Ok(hash) => hash,
|
||||
Err(_) => {
|
||||
return Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: "Error hashing password".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Insert user into database
|
||||
let result = sqlx::query("INSERT INTO users (phone, username, password_hash) VALUES (?, ?, ?)")
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.username)
|
||||
.bind(&password_hash)
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// Generate session token
|
||||
let token = utils::generate_token();
|
||||
let expires_at = Utc::now() + Duration::days(30);
|
||||
|
||||
// Store session
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO sessions (token, username, phone, expires_at) VALUES (?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&token)
|
||||
.bind(&payload.username)
|
||||
.bind(&payload.phone)
|
||||
.bind(expires_at.naive_utc())
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
Json(model::AuthResponse {
|
||||
success: true,
|
||||
message: "Registration successful".to_string(),
|
||||
username: Some(payload.username),
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
let error_message = if e
|
||||
.to_string()
|
||||
.contains("UNIQUE constraint failed: users.phone")
|
||||
{
|
||||
"Phone number already registered".to_string()
|
||||
} else if e
|
||||
.to_string()
|
||||
.contains("UNIQUE constraint failed: users.username")
|
||||
{
|
||||
"Username already taken".to_string()
|
||||
} else {
|
||||
"Registration failed".to_string()
|
||||
};
|
||||
|
||||
Json(model::AuthResponse {
|
||||
success: false,
|
||||
message: error_message,
|
||||
username: None,
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use axum::extract::{Json, Query, State};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
|
||||
// New endpoint to search users
|
||||
pub async fn search_users_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
Query(params): Query<model::SearchQuery>,
|
||||
// headers: axum::http::HeaderMap,
|
||||
) -> Json<Vec<model::ChatPartner>> {
|
||||
let search_pattern = format!("%{}%", params.q);
|
||||
|
||||
let users = sqlx::query(
|
||||
r#"
|
||||
SELECT username, phone, avatar_url
|
||||
FROM users
|
||||
WHERE (username LIKE ? OR phone LIKE ?)
|
||||
LIMIT 20
|
||||
"#,
|
||||
)
|
||||
.bind(&search_pattern)
|
||||
.bind(&search_pattern)
|
||||
.fetch_all(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match users {
|
||||
Ok(rows) => {
|
||||
let mut results = Vec::new();
|
||||
for row in rows {
|
||||
results.push(model::ChatPartner {
|
||||
username: row.get("username"),
|
||||
phone: row.get("phone"),
|
||||
avatar_url: row.get("avatar_url"),
|
||||
last_message: None,
|
||||
last_message_time: None,
|
||||
unread_count: 0,
|
||||
});
|
||||
}
|
||||
Json(results)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error searching users: {}", e);
|
||||
Json(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Json, State};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn validate_token_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
Json(payload): Json<model::ValidateTokenRequest>,
|
||||
) -> Json<model::ValidateTokenResponse> {
|
||||
match utils::verify_token(&state.db_pool, &payload.token).await {
|
||||
Ok(Some(username)) => {
|
||||
// Get user details
|
||||
let user_result = sqlx::query("SELECT phone, avatar_url FROM users WHERE username = ?")
|
||||
.bind(&username)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(Some(row)) => {
|
||||
let phone: String = row.get("phone");
|
||||
let avatar_url: Option<String> = row.get("avatar_url");
|
||||
Json(model::ValidateTokenResponse {
|
||||
success: true,
|
||||
username: Some(username),
|
||||
phone: Some(phone),
|
||||
avatar_url,
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
Ok(None) => Json(model::ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
avatar_url: None,
|
||||
message: Some("User not found".to_string()),
|
||||
}),
|
||||
Err(_) => Json(model::ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
avatar_url: None,
|
||||
message: Some("Database error".to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
Ok(None) => Json(model::ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
avatar_url: None,
|
||||
message: Some("Invalid or expired token".to_string()),
|
||||
}),
|
||||
Err(_) => Json(model::ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
avatar_url: None,
|
||||
message: Some("Token verification error".to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
use crate::model;
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::{
|
||||
extract::{
|
||||
State,
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
},
|
||||
response::Response,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// WebSocket handlers
|
||||
pub async fn websocket_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
) -> Response {
|
||||
ws.on_upgrade(|socket| handle_websocket(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_websocket(socket: WebSocket, state: Arc<state::AppState>) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
// Handle authentication
|
||||
let username = match receiver.next().await {
|
||||
Some(Ok(Message::Text(auth_msg))) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&auth_msg) {
|
||||
Ok(auth) => {
|
||||
if let (Some(token), Some(auth_type)) =
|
||||
(auth["token"].as_str(), auth["type"].as_str())
|
||||
{
|
||||
if auth_type == "auth" {
|
||||
match utils::verify_token(&state.db_pool, token).await {
|
||||
Ok(Some(user_username)) => {
|
||||
if state.users.read().await.contains_key(&user_username) {
|
||||
let _ = sender
|
||||
.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"content": "User already connected"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
state.users.write().await.insert(user_username.clone(), tx);
|
||||
|
||||
let _ = sender
|
||||
.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "system",
|
||||
"content": "Connected successfully"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.await;
|
||||
|
||||
utils::broadcast_user_list(&state.users).await;
|
||||
|
||||
if let Ok(messages) = utils::get_message_history(
|
||||
&state.db_pool,
|
||||
&user_username,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
{
|
||||
for msg in messages {
|
||||
let message_json = serde_json::to_string(&msg).unwrap();
|
||||
let _ = sender.send(Message::Text(message_json)).await;
|
||||
}
|
||||
}
|
||||
|
||||
user_username
|
||||
}
|
||||
Ok(None) => {
|
||||
let _ = sender
|
||||
.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"content": "Invalid or expired token"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = sender
|
||||
.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"content": "Authentication error"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => return,
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
println!("User {} connected", username);
|
||||
|
||||
let state_clone = state.clone();
|
||||
let username_clone = username.clone();
|
||||
|
||||
let mut send_sender = sender;
|
||||
let send_task = tokio::spawn(async move {
|
||||
while let Some(message) = rx.recv().await {
|
||||
if send_sender.send(message).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let recv_state = state.clone();
|
||||
let recv_task = tokio::spawn(async move {
|
||||
while let Some(result) = receiver.next().await {
|
||||
match result {
|
||||
Ok(Message::Text(text)) => {
|
||||
if let Ok(mut chat_msg) = serde_json::from_str::<model::ChatMessage>(&text) {
|
||||
chat_msg.from_user = username.clone();
|
||||
|
||||
if let Err(e) = utils::save_message(&recv_state.db_pool, &chat_msg).await {
|
||||
eprintln!("Failed to save message: {}", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
let users = recv_state.users.read().await;
|
||||
if chat_msg.to_user != chat_msg.from_user {
|
||||
if let Some(recipient_tx) = users.get(&chat_msg.to_user) {
|
||||
let message_json = serde_json::to_string(&chat_msg).unwrap();
|
||||
let _ = recipient_tx.send(Message::Text(message_json));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sender_tx) = users.get(&username) {
|
||||
let message_json = serde_json::to_string(&chat_msg).unwrap();
|
||||
let _ = sender_tx.send(Message::Text(message_json));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => break,
|
||||
Err(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = send_task => {},
|
||||
_ = recv_task => {},
|
||||
}
|
||||
|
||||
state_clone.users.write().await.remove(&username_clone);
|
||||
utils::broadcast_user_list(&state_clone.users).await;
|
||||
println!("User {} disconnected", username_clone);
|
||||
}
|
||||
Reference in new issue
Block a user