diff --git a/CHANGELOG.md b/CHANGELOG.md index 1faaeaa..ace35a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## [0.5.3] - 2025-09-02 + +### 🔄 Архитектурные изменения +- **УПРОЩЕНО**: Убран сложный роутинг Actix-web в пользу универсального обработчика +- **ДОБАВЛЕНО**: Прямое определение HTTP методов (GET/POST) в единой точке +- **УБРАНО**: HTTP API для управления квотами (quota endpoints) +- **СОХРАНЕНО**: ACME challenge поддержка для SSL сертификатов + +### 📋 API Структура +- `GET /` - авторизованная информация о персональном хранилище +- `GET /` - статические файлы с миниатюрами +- `POST /` - авторизованная загрузка файлов + +### 🔧 Технические детали +- Единый `universal_handler` для всех запросов +- Определение метода через `req.method()` +- Маршрутизация по пути через `req.path()` +- CORS и middleware сохранены + ## [0.5.2] - 2025-09-02 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 04fb399..4e9fef9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,7 +117,7 @@ dependencies = [ "actix-multipart-derive", "actix-utils", "actix-web", - "derive_more 0.99.18", + "derive_more 0.99.20", "futures-core", "futures-util", "httparse", @@ -855,9 +855,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1106,18 +1106,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -1137,9 +1137,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -1147,9 +1147,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -1161,9 +1161,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -1201,9 +1201,9 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.18" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "convert_case", "proc-macro2", @@ -2644,7 +2644,7 @@ dependencies = [ [[package]] name = "quoter" -version = "0.5.1" +version = "0.5.3" dependencies = [ "actix", "actix-cors", @@ -3787,9 +3787,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -4277,27 +4277,27 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.1" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index ac9113f..dde7b8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quoter" -version = "0.5.1" +version = "0.5.3" edition = "2024" [dependencies] @@ -8,7 +8,6 @@ futures = "0.3.30" serde_json = "1.0.143" actix-web = "4.11.0" actix-cors = "0.7.0" -actix-files = "0.6.7" reqwest = { version = "0.12.23", features = ["json"] } sentry = { version = "0.42", features = ["tokio"] } uuid = { version = "1.18.0", features = ["v4"] } diff --git a/src/app_state.rs b/src/app_state.rs index 91ae222..c666485 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -179,42 +179,4 @@ impl AppState { Ok(new_quota) } - - /// Устанавливает квоту пользователя в байтах (позволяет увеличить или уменьшить) - pub async fn set_user_quota(&self, user_id: &str, bytes: u64) -> Result { - let mut redis = self.redis.clone(); - let quota_key = format!("quota:{}", user_id); - - // Устанавливаем новое значение квоты - redis - .set::<_, u64, ()>("a_key, bytes) - .await - .map_err(|_| ErrorInternalServerError("Failed to set user quota in Redis"))?; - - Ok(bytes) - } - - /// Увеличивает квоту пользователя на указанное количество байт - pub async fn increase_user_quota( - &self, - user_id: &str, - additional_bytes: u64, - ) -> Result { - let mut redis = self.redis.clone(); - let quota_key = format!("quota:{}", user_id); - - // Получаем текущую квоту - let current_quota: u64 = redis.get("a_key).await.unwrap_or(0); - - // Вычисляем новую квоту - let new_quota = current_quota + additional_bytes; - - // Устанавливаем новое значение - redis - .set::<_, u64, ()>("a_key, new_quota) - .await - .map_err(|_| ErrorInternalServerError("Failed to increase user quota in Redis"))?; - - Ok(new_quota) - } } diff --git a/src/auth.rs b/src/auth.rs index 5df56eb..ab3da48 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2,33 +2,8 @@ use actix_web::error::ErrorInternalServerError; use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; use log::{info, warn}; use redis::{AsyncCommands, aio::MultiplexedConnection}; -use reqwest::Client as HTTPClient; -use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::{collections::HashMap, env, error::Error}; - -// Старые структуры для совместимости с get_id_by_token -#[derive(Deserialize)] -struct AuthResponse { - data: Option, -} - -#[derive(Deserialize)] -struct AuthData { - validate_jwt_token: Option, -} - -#[derive(Deserialize)] -struct ValidateJWTToken { - is_valid: bool, - claims: Option, -} - -#[derive(Deserialize)] -struct Claims { - sub: Option, -} +use std::error::Error; // Структуры для JWT токенов #[derive(Debug, Deserialize)] @@ -51,57 +26,6 @@ pub struct Author { pub device_info: Option, } -/// Получает айди пользователя из токена в заголовке -#[allow(clippy::collapsible_if)] -pub async fn get_id_by_token(token: &str) -> Result> { - let auth_api_base = env::var("CORE_URL")?; - let query_name = "validate_jwt_token"; - let operation = "ValidateToken"; - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - - let mut variables = HashMap::>::new(); - let mut params = HashMap::::new(); - params.insert("token".to_string(), token.to_string()); - params.insert("token_type".to_string(), "access_token".to_string()); - variables.insert("params".to_string(), params); - - let gql = json!({ - "query": format!("query {}($params: ValidateJWTTokenInput!) {{ {}(params: $params) {{ is_valid claims }} }}", operation, query_name), - "operationName": operation, - "variables": variables - }); - - let client = HTTPClient::new(); - let response = client - .post(&auth_api_base) - .headers(headers) - .json(&gql) - .send() - .await?; - - if response.status().is_success() { - let auth_response: AuthResponse = response.json().await?; - if let Some(auth_data) = auth_response.data { - if let Some(validate_jwt_token) = auth_data.validate_jwt_token { - if validate_jwt_token.is_valid { - if let Some(claims) = validate_jwt_token.claims { - if let Some(sub) = claims.sub { - return Ok(sub); - } - } - } - } - } - Err(Box::new(std::io::Error::other("Invalid token response"))) - } else { - Err(Box::new(std::io::Error::other(format!( - "Request failed with status: {}", - response.status() - )))) - } -} - /// Декодирует JWT токен и извлекает claims с проверкой истечения fn decode_jwt_token(token: &str) -> Result> { // В реальном приложении здесь должен быть настоящий секретный ключ diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 35ba3c7..75ff23d 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,13 +1,10 @@ mod proxy; -mod quota; mod serve_file; mod upload; mod user; +mod universal; -pub use proxy::proxy_handler; -pub use quota::{get_quota_handler, increase_quota_handler, set_quota_handler}; -pub use upload::upload_handler; -pub use user::get_current_user_handler; +pub use universal::universal_handler; -// Общий лимит квоты на пользователя: 5 ГБ -pub const MAX_USER_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024; +// Общий лимит квоты на пользователя: 12 ГБ +pub const MAX_USER_QUOTA_BYTES: u64 = 12 * 1024 * 1024 * 1024; diff --git a/src/handlers/quota.rs b/src/handlers/quota.rs deleted file mode 100644 index 6bed956..0000000 --- a/src/handlers/quota.rs +++ /dev/null @@ -1,166 +0,0 @@ -use actix_web::{HttpRequest, HttpResponse, Result, web}; -use log::warn; -use serde::{Deserialize, Serialize}; - -use crate::app_state::AppState; -use crate::auth::get_id_by_token; - -#[derive(Deserialize)] -pub struct QuotaRequest { - pub user_id: String, - pub additional_bytes: Option, - pub new_quota_bytes: Option, -} - -#[derive(Serialize)] -pub struct QuotaResponse { - pub user_id: String, - pub current_quota: u64, - pub max_quota: u64, -} - -/// Обработчик для получения информации о квоте пользователя -pub async fn get_quota_handler( - req: HttpRequest, - state: web::Data, -) -> Result { - // Проверяем авторизацию - let token = req - .headers() - .get("Authorization") - .and_then(|header_value| header_value.to_str().ok()); - - if token.is_none() { - return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); - } - - let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| { - let error_msg = if e.to_string().contains("expired") { - "Admin token has expired" - } else { - "Invalid admin token" - }; - actix_web::error::ErrorUnauthorized(error_msg) - })?; - - // Получаем user_id из query параметров - let user_id = req - .query_string() - .split("user_id=") - .nth(1) - .and_then(|s| s.split('&').next()) - .ok_or_else(|| actix_web::error::ErrorBadRequest("Missing user_id parameter"))?; - - // Получаем текущую квоту пользователя - let current_quota = state.get_or_create_quota(user_id).await?; - - let response = QuotaResponse { - user_id: user_id.to_string(), - current_quota, - max_quota: crate::handlers::MAX_USER_QUOTA_BYTES, - }; - - Ok(HttpResponse::Ok().json(response)) -} - -/// Обработчик для увеличения квоты пользователя -pub async fn increase_quota_handler( - req: HttpRequest, - quota_data: web::Json, - state: web::Data, -) -> Result { - // Проверяем авторизацию - let token = req - .headers() - .get("Authorization") - .and_then(|header_value| header_value.to_str().ok()); - - if token.is_none() { - return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); - } - - let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| { - let error_msg = if e.to_string().contains("expired") { - "Admin token has expired" - } else { - "Invalid admin token" - }; - actix_web::error::ErrorUnauthorized(error_msg) - })?; - - let additional_bytes = quota_data - .additional_bytes - .ok_or_else(|| actix_web::error::ErrorBadRequest("Missing additional_bytes parameter"))?; - - if additional_bytes == 0 { - return Err(actix_web::error::ErrorBadRequest( - "additional_bytes must be greater than 0", - )); - } - - // Увеличиваем квоту пользователя - let new_quota = state - .increase_user_quota("a_data.user_id, additional_bytes) - .await?; - - warn!( - "Increased quota for user {} by {} bytes, new total: {} bytes", - quota_data.user_id, additional_bytes, new_quota - ); - - let response = QuotaResponse { - user_id: quota_data.user_id.clone(), - current_quota: new_quota, - max_quota: crate::handlers::MAX_USER_QUOTA_BYTES, - }; - - Ok(HttpResponse::Ok().json(response)) -} - -/// Обработчик для установки квоты пользователя -pub async fn set_quota_handler( - req: HttpRequest, - quota_data: web::Json, - state: web::Data, -) -> Result { - // Проверяем авторизацию - let token = req - .headers() - .get("Authorization") - .and_then(|header_value| header_value.to_str().ok()); - - if token.is_none() { - return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); - } - - let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| { - let error_msg = if e.to_string().contains("expired") { - "Admin token has expired" - } else { - "Invalid admin token" - }; - actix_web::error::ErrorUnauthorized(error_msg) - })?; - - let new_quota_bytes = quota_data - .new_quota_bytes - .ok_or_else(|| actix_web::error::ErrorBadRequest("Missing new_quota_bytes parameter"))?; - - // Устанавливаем новую квоту пользователя - let new_quota = state - .set_user_quota("a_data.user_id, new_quota_bytes) - .await?; - - warn!( - "Set quota for user {} to {} bytes", - quota_data.user_id, new_quota - ); - - let response = QuotaResponse { - user_id: quota_data.user_id.clone(), - current_quota: new_quota, - max_quota: crate::handlers::MAX_USER_QUOTA_BYTES, - }; - - Ok(HttpResponse::Ok().json(response)) -} diff --git a/src/handlers/universal.rs b/src/handlers/universal.rs new file mode 100644 index 0000000..aeaaa64 --- /dev/null +++ b/src/handlers/universal.rs @@ -0,0 +1,68 @@ +use actix_web::{HttpRequest, HttpResponse, Result, web}; +use actix_multipart::Multipart; +use log::{info, warn}; + +use crate::app_state::AppState; + +/// Универсальный обработчик, который определяет HTTP метод и путь +pub async fn universal_handler( + req: HttpRequest, + payload: web::Payload, + state: web::Data, +) -> Result { + let method = req.method().clone(); + let path = req.path().to_string(); + + info!("Universal handler: {} {}", method, path); + + // Возвращаем 404 для .well-known путей (для Let's Encrypt ACME) + if path.starts_with("/.well-known/") { + warn!("ACME challenge path requested: {}", path); + return Ok(HttpResponse::NotFound().finish()); + } + + match method.as_str() { + "GET" => handle_get(req, state, &path).await, + "POST" => handle_post(req, payload, state, &path).await, + _ => { + warn!("Unsupported HTTP method: {}", method); + Ok(HttpResponse::MethodNotAllowed().json(serde_json::json!({ + "error": "Method not allowed" + }))) + } + } +} + +async fn handle_get( + req: HttpRequest, + state: web::Data, + path: &str, +) -> Result { + if path == "/" { + // GET / - получение информации о пользователе + crate::handlers::user::get_current_user_handler(req, state).await + } else { + // GET /{path} - получение файла через proxy + let path_without_slash = path.trim_start_matches('/'); + let requested_res = web::Path::from(path_without_slash.to_string()); + crate::handlers::proxy::proxy_handler(req, requested_res, state).await + } +} + +async fn handle_post( + req: HttpRequest, + payload: web::Payload, + state: web::Data, + path: &str, +) -> Result { + if path == "/" { + // POST / - загрузка файла (multipart) + let multipart = Multipart::new(&req.headers(), payload); + crate::handlers::upload::upload_handler(req, multipart, state).await + } else { + warn!("Unsupported POST path: {}", path); + Ok(HttpResponse::NotFound().json(serde_json::json!({ + "error": "Endpoint not found" + }))) + } +} diff --git a/src/main.rs b/src/main.rs index cab10a9..f9c7bf6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,10 +14,7 @@ use actix_web::{ }; use app_state::AppState; -use handlers::{ - get_current_user_handler, get_quota_handler, increase_quota_handler, proxy_handler, - set_quota_handler, upload_handler, -}; +use handlers::universal_handler; use log::warn; use std::env; use tokio::task::spawn_blocking; @@ -63,19 +60,7 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(app_state.clone())) .wrap(cors) .wrap(Logger::default()) - .route("/", web::get().to(get_current_user_handler)) - .route("/", web::post().to(upload_handler)) - .route("/quota", web::get().to(get_quota_handler)) - .route("/quota/increase", web::post().to(increase_quota_handler)) - .route("/quota/set", web::post().to(set_quota_handler)) - .service( - web::scope("/.well-known") - .service( - actix_files::Files::new("/", "/tmp/.well-known") - .show_files_listing() - ) - ) - .route("/{path:.*}", web::get().to(proxy_handler)) + .default_service(web::to(universal_handler)) }) .bind(addr)? .run()