diff --git a/Cargo.lock b/Cargo.lock index f55d71d..a710943 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -1066,6 +1075,8 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1279,6 +1290,8 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", "uuid", ] @@ -1604,6 +1617,30 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1696,6 +1733,35 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + [[package]] name = "ron" version = "0.7.1" @@ -1727,6 +1793,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.111", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.18.0" @@ -1762,6 +1862,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -2801,6 +2910,47 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.111", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b39868d43c011961e04b41623e050aedf2cc93652562ff7935ce0f819aaf2da" +dependencies = [ + "axum", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.18.1" @@ -2830,6 +2980,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2912,6 +3072,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -3244,6 +3413,18 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index 899806e..2b3a5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,5 @@ uuid = { version = "1.0", features = ["v4"] } image = "0.24" config = "0.13" symphonia = { version = "0.5", features = ["all"] } +utoipa = { version = "4.2", features = ["axum_extras", "chrono"] } +utoipa-swagger-ui = { version = "6.0", features = ["axum"] } diff --git a/src/api/chats.rs b/src/api/chats.rs index 88ddfc9..7ffed8b 100644 --- a/src/api/chats.rs +++ b/src/api/chats.rs @@ -6,6 +6,15 @@ use sqlx::Row; use std::sync::Arc; // УПРОЩЕННАЯ версия получения чатов +#[utoipa::path( + get, + path = "/api/chats", + responses( + (status = 200, description = "List of chat partners", body = Vec), + (status = 401, description = "Unauthorized", body = Vec) + ), + security(("bearerAuth" = [])) +)] pub async fn get_chats_handler( State(state): State>, headers: axum::http::HeaderMap, diff --git a/src/api/login.rs b/src/api/login.rs index 7574cd5..e603e6a 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -7,6 +7,15 @@ use chrono::{Duration, Utc}; use sqlx::Row; use std::sync::Arc; +#[utoipa::path( + post, + path = "/api/login", + request_body = LoginRequest, + responses( + (status = 200, description = "Login successful", body = AuthResponse), + (status = 400, description = "Invalid credentials", body = AuthResponse) + ) +)] pub async fn login_handler( State(state): State>, Json(payload): Json, diff --git a/src/api/messages.rs b/src/api/messages.rs index 18da808..cb13134 100644 --- a/src/api/messages.rs +++ b/src/api/messages.rs @@ -6,6 +6,16 @@ use axum::extract::{Json, State}; use std::sync::Arc; // Mark messages as read endpoint +#[utoipa::path( + post, + path = "/api/mark-read", + request_body = MarkReadRequest, + responses( + (status = 200, description = "Messages marked as read", body = serde_json::Value), + (status = 401, description = "Unauthorized", body = serde_json::Value) + ), + security(("bearerAuth" = [])) +)] pub async fn mark_read_handler( State(state): State>, headers: axum::http::HeaderMap, diff --git a/src/api/messages/delete.rs b/src/api/messages/delete.rs index 33e0559..7e91f92 100644 --- a/src/api/messages/delete.rs +++ b/src/api/messages/delete.rs @@ -7,6 +7,16 @@ use std::sync::Arc; use tokio::fs; // Delete message endpoint +#[utoipa::path( + post, + path = "/api/delete-message", + request_body = DeleteMessageRequest, + responses( + (status = 200, description = "Message deletion result", body = DeleteMessageResponse), + (status = 401, description = "Unauthorized", body = DeleteMessageResponse) + ), + security(("bearerAuth" = [])) +)] pub async fn delete_message_handler( State(state): State>, headers: axum::http::HeaderMap, diff --git a/src/api/profile/avatar.rs b/src/api/profile/avatar.rs index 4cff39d..b27d5af 100644 --- a/src/api/profile/avatar.rs +++ b/src/api/profile/avatar.rs @@ -6,6 +6,16 @@ use std::sync::Arc; use uuid::Uuid; // Upload avatar endpoint +#[utoipa::path( + post, + path = "/api/upload-avatar", + request_body(content = Vec, description = "Avatar image file", content_type = "multipart/form-data"), + responses( + (status = 200, description = "Avatar upload result", body = UploadAvatarResponse), + (status = 401, description = "Unauthorized", body = UploadAvatarResponse) + ), + security(("bearerAuth" = [])) +)] pub async fn upload_avatar_handler( State(state): State>, headers: axum::http::HeaderMap, diff --git a/src/api/profile/get.rs b/src/api/profile/get.rs index ac22f29..729f89b 100644 --- a/src/api/profile/get.rs +++ b/src/api/profile/get.rs @@ -5,6 +5,15 @@ use axum::extract::{Json, State}; use sqlx::Row; use std::sync::Arc; +#[utoipa::path( + get, + path = "/api/profile", + responses( + (status = 200, description = "User profile information", body = UserProfile), + (status = 401, description = "Unauthorized", body = UserProfile) + ), + security(("bearerAuth" = [])) +)] pub async fn profile_handler( State(state): State>, headers: axum::http::HeaderMap, diff --git a/src/api/register.rs b/src/api/register.rs index 8e309da..1bc1830 100644 --- a/src/api/register.rs +++ b/src/api/register.rs @@ -6,6 +6,15 @@ use bcrypt::{DEFAULT_COST, hash}; use chrono::{Duration, Utc}; use std::sync::Arc; +#[utoipa::path( + post, + path = "/api/register", + request_body = RegisterRequest, + responses( + (status = 200, description = "Registration successful", body = AuthResponse), + (status = 400, description = "Registration failed", body = AuthResponse) + ) +)] pub async fn register_handler( State(state): State>, Json(payload): Json, diff --git a/src/api/search.rs b/src/api/search.rs index ae8e89a..da8721c 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -5,6 +5,16 @@ use sqlx::Row; use std::sync::Arc; // New endpoint to search users +#[utoipa::path( + get, + path = "/api/search-users", + params( + ("q" = String, Query, description = "Search query string") + ), + responses( + (status = 200, description = "List of matching users", body = Vec) + ) +)] pub async fn search_users_handler( State(state): State>, Query(params): Query, diff --git a/src/api/security.rs b/src/api/security.rs index 374c64f..9f38b56 100644 --- a/src/api/security.rs +++ b/src/api/security.rs @@ -5,6 +5,15 @@ use axum::extract::{Json, State}; use sqlx::Row; use std::sync::Arc; +#[utoipa::path( + post, + path = "/api/validate-token", + request_body = ValidateTokenRequest, + responses( + (status = 200, description = "Token validation result", body = ValidateTokenResponse), + (status = 400, description = "Invalid token", body = ValidateTokenResponse) + ) +)] pub async fn validate_token_handler( State(state): State>, Json(payload): Json, diff --git a/src/api/upload.rs b/src/api/upload.rs index 877dc78..47e9cf4 100644 --- a/src/api/upload.rs +++ b/src/api/upload.rs @@ -12,6 +12,16 @@ use symphonia::core::probe::Hint; use tokio::fs; use tokio::io::AsyncWriteExt; +#[utoipa::path( + post, + path = "/api/upload", + request_body(content = Vec, description = "File to upload", content_type = "multipart/form-data"), + responses( + (status = 200, description = "File upload result", body = serde_json::Value), + (status = 401, description = "Unauthorized", body = serde_json::Value) + ), + security(("bearerAuth" = [])) +)] pub async fn upload_file_handler( State(state): State>, headers: axum::http::HeaderMap, diff --git a/src/api/websock.rs b/src/api/websock.rs index c2600d9..c0d5548 100644 --- a/src/api/websock.rs +++ b/src/api/websock.rs @@ -12,7 +12,27 @@ use futures_util::{SinkExt, StreamExt}; use std::sync::Arc; use tokio::sync::mpsc; -// WebSocket handlers +/// WebSocket endpoint for real-time chat messaging +/// +/// ## Connection Flow +/// 1. Client connects to `/ws` +/// 2. Client sends authentication message: `{"type": "auth", "token": "your_token"}` +/// 3. Server responds with system message on success or error message on failure +/// 4. After successful authentication, client can send/receive chat messages +/// +/// ## Message Types +/// - Authentication: `{"type": "auth", "token": "jwt_token"}` +/// - Chat message: `{"id": "...", "from_user": "...", "to_user": "...", "content": "...", "timestamp": 12345, "message_type": "text"}` +/// - System message: `{"type": "system", "content": "Connected successfully"}` +/// - Error message: `{"type": "error", "content": "Invalid token"}` +#[utoipa::path( + get, + path = "/ws", + responses( + (status = 101, description = "WebSocket upgrade successful") + ), + tag = "WebSocket" +)] pub async fn websocket_handler( ws: WebSocketUpgrade, State(state): State>, diff --git a/src/main.rs b/src/main.rs index 0c7e6fb..4029666 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,13 +6,17 @@ use axum::{ use std::{collections::HashMap, net::SocketAddr, sync::Arc}; use tokio::sync::RwLock; use tower_http::services::ServeDir; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; mod api; mod conf; mod db; mod model; +mod openapi; mod state; mod utils; +mod websocket_doc; #[tokio::main] async fn main() -> Result<(), Box> { @@ -64,6 +68,9 @@ async fn main() -> Result<(), Box> { ) .route("/api/upload", post(api::upload::upload_file_handler)) .route("/api/profile", get(api::profile::get::profile_handler)) + .merge( + SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi::ApiDoc::openapi()), + ) .nest_service("/", ServeDir::new(&state.config.files.web_dir)) .layer(DefaultBodyLimit::max(state.config.files.max_upload_size)) .with_state(state.clone()); diff --git a/src/model.rs b/src/model.rs index 97b276a..05f880f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,108 +1,154 @@ use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct AuthResponse { + /// Indicates if the operation was successful pub success: bool, + /// Message describing the result pub message: String, + /// Username of the authenticated user pub username: Option, + /// Authentication token pub token: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct RegisterRequest { + /// Phone number for registration pub phone: String, + /// Desired username pub username: String, + /// Password for account pub password: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct LoginRequest { + /// Phone number associated with the account pub phone: String, + /// Password for the account pub password: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct ValidateTokenResponse { + /// Indicates if the token is valid pub success: bool, + /// Username associated with the token pub username: Option, + /// Phone number associated with the token pub phone: Option, + /// URL to the user's avatar pub avatar_url: Option, + /// Additional message about the validation pub message: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct ValidateTokenRequest { + /// Authentication token to validate pub token: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct ChatPartner { + /// Username of the chat partner pub username: String, + /// Phone number of the chat partner pub phone: String, + /// URL to the partner's avatar pub avatar_url: Option, + /// Content of the last message pub last_message: Option, + /// Timestamp of the last message pub last_message_time: Option, + /// Number of unread messages pub unread_count: i64, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct SearchQuery { + /// Search query string pub q: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct MarkReadRequest { + /// User who sent the messages pub from_user: String, + /// User who received the messages pub to_user: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct DeleteMessageRequest { + /// ID of the message to delete pub message_id: String, + /// Whether to delete for everyone pub for_everyone: bool, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct DeleteMessageResponse { + /// Indicates if the deletion was successful pub success: bool, + /// Message describing the result pub message: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct UploadAvatarResponse { + /// Indicates if the upload was successful pub success: bool, + /// URL to the uploaded avatar pub avatar_url: Option, + /// Message describing the result pub message: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct UserProfile { + /// Username of the profile pub username: String, + /// Phone number of the profile pub phone: String, + /// URL to the user's avatar pub avatar_url: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] pub enum MessageType { + /// Text message #[serde(rename = "text")] Text, + /// File attachment #[serde(rename = "file")] File, + /// Photo attachment #[serde(rename = "photo")] Photo, + /// Video attachment #[serde(rename = "video")] Video, + /// Audio attachment #[serde(rename = "audio")] Audio, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] pub struct ChatMessage { + /// Unique identifier for the message pub id: String, + /// User who sent the message pub from_user: String, + /// User who received the message pub to_user: String, + /// Content of the message pub content: String, // Store serialized content as JSON string + /// Timestamp of when the message was sent pub timestamp: i64, + /// Type of the message pub message_type: MessageType, } @@ -214,38 +260,110 @@ pub struct ChatMessage { // Video(VideoInfo), // } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] pub struct FileInfo { + /// Original filename pub filename: String, + /// Path to the stored file pub filepath: String, + /// Size of the file in bytes pub size: u64, + /// MIME type of the file pub mime_type: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] pub struct PhotoInfo { + /// Original filename pub filename: String, + /// Path to the stored photo pub filepath: String, + /// Size of the photo in bytes pub size: u64, + /// Width of the photo in pixels pub width: u32, + /// Height of the photo in pixels pub height: u32, + /// MIME type of the photo pub mime_type: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] pub struct VideoInfo { + /// Original filename pub filename: String, + /// Path to the stored video pub filepath: String, + /// Size of the video in bytes pub size: u64, + /// Duration of the video in seconds pub duration: Option, // in seconds + /// MIME type of the video pub mime_type: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] pub struct AudioInfo { + /// Original filename pub filename: String, + /// Path to the stored audio pub filepath: String, + /// Size of the audio in bytes pub size: u64, + /// Duration of the audio in seconds pub duration: Option, // in seconds + /// MIME type of the audio pub mime_type: String, } + +// WebSocket message types +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct WsAuthMessage { + /// Type of the message (should be "auth") + #[schema(example = "auth")] + pub r#type: String, + /// Authentication token + #[schema(example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")] + pub token: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct WsChatMessage { + /// Unique identifier for the message + #[schema(example = "123e4567-e89b-12d3-a456-426614174000")] + pub id: String, + /// User who sent the message + #[schema(example = "john_doe")] + pub from_user: String, + /// User who received the message + #[schema(example = "jane_doe")] + pub to_user: String, + /// Content of the message + #[schema(example = "Hello, world!")] + pub content: String, + /// Timestamp of when the message was sent + #[schema(example = 1678886400)] + pub timestamp: i64, + /// Type of the message + pub message_type: MessageType, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct WsSystemMessage { + /// Type of the system message + #[schema(example = "system")] + pub r#type: String, + /// Content of the system message + #[schema(example = "Connected successfully")] + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct WsErrorMessage { + /// Type of the error message + #[schema(example = "error")] + pub r#type: String, + /// Content of the error message + #[schema(example = "Invalid or expired token")] + pub content: String, +} diff --git a/src/openapi.rs b/src/openapi.rs new file mode 100644 index 0000000..df6b5de --- /dev/null +++ b/src/openapi.rs @@ -0,0 +1,74 @@ +use crate::api; +use crate::model::*; +use crate::websocket_doc::WebSocketApiDoc; +use utoipa::OpenApi; + +/// Chat messaging API +#[derive(OpenApi)] +#[openapi( + info( + title = "Chat Messaging API", + description = "A simple chat messaging API with user registration, login, and message handling.", + version = "1.0.0" + ), + paths( + api::websock::websocket_handler, + api::login::login_handler, + api::register::register_handler, + api::security::validate_token_handler, + api::chats::get_chats_handler, + api::search::search_users_handler, + api::messages::mark_read_handler, + api::messages::delete::delete_message_handler, + api::profile::get::profile_handler, + api::profile::avatar::upload_avatar_handler, + api::upload::upload_file_handler + ), + components( + schemas( + AuthResponse, + RegisterRequest, + LoginRequest, + ValidateTokenResponse, + ValidateTokenRequest, + ChatPartner, + SearchQuery, + MarkReadRequest, + DeleteMessageRequest, + DeleteMessageResponse, + UploadAvatarResponse, + UserProfile, + MessageType, + ChatMessage, + FileInfo, + PhotoInfo, + VideoInfo, + AudioInfo, + WsAuthMessage, + WsChatMessage, + WsSystemMessage, + WsErrorMessage + ) + ), + modifiers(&SecurityAddon, &WebSocketApiDoc) +)] +pub struct ApiDoc; + +struct SecurityAddon; + +impl utoipa::Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}; + components.add_security_scheme( + "bearerAuth", + SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .bearer_format("JWT") + .build(), + ), + ); + } + } +} diff --git a/src/websocket_doc.rs b/src/websocket_doc.rs new file mode 100644 index 0000000..28b5b8e --- /dev/null +++ b/src/websocket_doc.rs @@ -0,0 +1,28 @@ +use utoipa::Modify; + +/// Расширенная документация для WebSocket API +pub struct WebSocketApiDoc; + +impl Modify for WebSocketApiDoc { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + // Обновляем описание, добавляя информацию о WebSocket + let current_description = openapi.info.description.clone().unwrap_or_default(); + let extended_description = format!( + "{}\n\n## WebSocket API\n\n### Connection Flow\n1. Connect to `/ws`\n2. Send auth message: `{{\"type\": \"auth\", \"token\": \"your_jwt\"}}`\n3. Receive system/error message\n4. Send/receive chat messages\n\n### Message Types:\n- **Auth**: `{{\"type\": \"auth\", \"token\": \"...\"}}`\n- **Chat**: `{{\"id\": \"...\", \"from_user\": \"...\", \"to_user\": \"...\", \"content\": \"...\", \"timestamp\": 12345, \"message_type\": \"text\"}}`\n- **System**: `{{\"type\": \"system\", \"content\": \"...\"}}`\n- **Error**: `{{\"type\": \"error\", \"content\": \"...\"}}`", + current_description + ); + openapi.info.description = Some(extended_description); + + // Добавляем тег для WebSocket + let websocket_tag = utoipa::openapi::tag::TagBuilder::new() + .name("WebSocket") + .description(Some("Real-time chat messaging via WebSocket")) + .build(); + + if let Some(tags) = &mut openapi.tags { + tags.push(websocket_tag); + } else { + openapi.tags = Some(vec![websocket_tag]); + } + } +}