use actix_multipart::Multipart; use actix_web::{HttpResponse, web}; use futures_util::StreamExt; use serde::Serialize; use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::error::AppError; use crate::service::AppService; use crate::session::Session; #[derive(Serialize, utoipa::ToSchema)] pub struct AvatarData { pub avatar_url: String, pub storage_key: String, } pub async fn parse_avatar_field( mut payload: Multipart, ) -> Result<(Vec, Option, Option), AppError> { while let Some(Ok(mut field)) = payload.next().await { if field.name() == Some("avatar") { let content_type = field.content_type().map(|m| m.to_string()); let file_name = field .content_disposition() .and_then(|cd| cd.get_filename().map(|s| s.to_string())); let mut data: Vec = Vec::new(); while let Some(Ok(chunk)) = field.next().await { data.extend_from_slice(&chunk); } return Ok((data, content_type, file_name)); } } Err(AppError::BadRequest( "missing 'avatar' field in multipart form".into(), )) } /// Upload user avatar /// /// Uploads a new avatar image for the authenticated user. /// Requires authentication. Accepts multipart/form-data with a single "avatar" field. /// /// Supported formats: PNG, JPEG, WebP, GIF (max 5MB). /// /// Effects: /// - Avatar image is stored in S3-compatible object storage /// - Previous avatar is deleted from storage /// - User's avatar URL is updated /// /// Returns the new avatar URL and storage key. #[utoipa::path( post, path = "/api/v1/user/account/avatar", tag = "User", operation_id = "userUploadAvatar", request_body( content_type = "multipart/form-data", description = "Avatar image file in a multipart form field named 'avatar'." ), responses( (status = 200, description = "Avatar uploaded successfully", body = ApiResponse), (status = 400, description = "Invalid parameters: unsupported file type or image too large", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 404, description = "User not found", body = ApiErrorResponse), (status = 500, description = "Internal server error or S3 storage failure", body = ApiErrorResponse), ), security( ("session_cookie" = []) ) )] pub async fn upload_avatar( service: web::Data, session: Session, payload: Multipart, ) -> Result { let (data, content_type, file_name) = parse_avatar_field(payload).await?; let (avatar_url, storage_key) = service .user .user_upload_avatar(&session, data, content_type, file_name) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new(AvatarData { avatar_url, storage_key, }))) }