use actix_multipart::Multipart; use actix_web::{HttpResponse, Result, web}; use futures_util::StreamExt; use service::AppService; use session::Session; #[derive(serde::Serialize, utoipa::ToSchema)] pub struct AvatarUploadResponse { pub avatar_url: String, } /// Upload current user's avatar. /// Accepts a multipart form with a single file field. #[utoipa::path( post, path = "/api/users/me/avatar", responses( (status = 200, description = "Avatar uploaded", body = crate::ApiResponse), (status = 401, description = "Unauthorized"), ), tag = "User" )] pub async fn upload_avatar( service: web::Data, session: Session, mut payload: Multipart, ) -> Result { let max_size: usize = 2 * 1024 * 1024; // 2MB let mut file_data: Vec = Vec::new(); let mut file_ext = "png".to_string(); while let Some(item) = payload.next().await { let mut field = item.map_err(|e| { crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) })?; // Detect file extension from content-type if let Some(content_type) = field.content_type() { let ext = match content_type.essence_str() { "image/jpeg" | "image/jpg" => "jpg", "image/gif" => "gif", "image/webp" => "webp", "image/png" | _ => "png", }; file_ext = ext.to_string(); } while let Some(chunk) = field.next().await { let data = chunk.map_err(|e| { crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) })?; if file_data.len() + data.len() > max_size { return Err(crate::error::ApiError( service::error::AppError::BadRequest( "File exceeds maximum size of 2MB".to_string(), ), )); } file_data.extend_from_slice(&data); } } if file_data.is_empty() { return Err(crate::error::ApiError( service::error::AppError::BadRequest("No file provided".to_string()), )); } let avatar_url = service .user_avatar_upload(session, file_data, &file_ext) .await .map_err(crate::error::ApiError::from)?; Ok(crate::ApiResponse::ok(AvatarUploadResponse { avatar_url }).to_response()) }