feat: Enhance CORS middleware to dynamically include development port and allow OPTIONS method

This commit is contained in:
MythEclipse
2025-08-17 16:06:21 +07:00
parent cb98440b65
commit 0b51a56dee
2 changed files with 75 additions and 68 deletions
+61 -58
View File
@@ -18,8 +18,8 @@ use std::pin::Pin;
use std::future::Future;
use anyhow::Result;
use tracing::warn;
use tracing::info;
use tracing::error;
use crate::v1::users::users_dto::{UsersDetailItemDto as UserDto, UsersCreateRequestDto as CreateUserDto};
use serde_json::json;
@@ -384,11 +384,16 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
let state = state.to_owned();
let user_id = user_id.to_owned();
Box::pin(async move {
info!("Entering upload_file function for user_id: {}", user_id);
// Initialize MinIO configuration
let minio_config = match MinioConfig::from_env() {
Ok(config) => config,
Ok(config) => {
info!("MinIO config loaded successfully.");
config
},
Err(e) => {
log::error!("Failed to load MinIO config: {}", e);
error!("Failed to load MinIO config: {}", e);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
"MinIO configuration error",
@@ -398,12 +403,16 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
// Store bucket name before minio_config is moved
let bucket_name = minio_config.bucket_name.clone();
info!("MinIO bucket name: {}", bucket_name);
// Initialize MinIO service
let minio_service = match create_minio_service_from_config(minio_config).await {
Ok(service) => service,
Ok(service) => {
info!("MinIO service initialized successfully.");
service
},
Err(e) => {
log::error!("Failed to initialize MinIO service: {}", e);
error!("Failed to initialize MinIO service: {}", e);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
"MinIO service initialization error",
@@ -411,52 +420,47 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
}
};
// Extract email from user_id (which contains email in SurrealDB format)
let user_email = user_id
.replace("app_users:", "")
.replace("", "")
.replace("", "");
// Get actual user data from database to get real user ID and old avatar URL
// Get actual user data from database using user_id (which is a UUID)
let repo = UsersRepository::new(&state);
let (actual_user_id, user_email, old_avatar_url) = match repo.query_user_by_email(user_email.clone()).await {
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &user_id);
let user_data = match repo.query_user_by_id(&thing_id).await {
Ok(user) => {
// Extract the actual ID from the user record
let actual_id = user.id.id.to_raw();
let old_avatar = user.avatar.clone(); // Get the old avatar URL
(actual_id, user.email, old_avatar)
info!("Found user in DB. User ID: {}, Email: {}", user.id.id.to_raw(), user.email);
user
}
Err(_) => {
Err(e) => {
error!("Failed to find user in DB for ID {}: {}", user_id, e);
return common_response(
StatusCode::NOT_FOUND,
"User not found",
);
}
};
let actual_user_id = user_data.id.id.to_raw();
let user_email = user_data.email;
let mut file_data: Option<Vec<u8>> = None;
let mut filename: Option<String> = None;
let mut content_type: Option<String> = None;
// Process multipart form
info!("Starting multipart form processing.");
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
let name = field.name().unwrap_or("").to_string();
info!("Processing multipart field: {}", name);
match name.as_str() {
"file" => {
if file_data.is_some() {
warn!("Multiple file fields detected for profile upload. Only the first one will be processed.");
}
filename = field.file_name().map(|s| s.to_string());
content_type = field.content_type().map(|s| s.to_string());
info!("Detected file field. Filename: {:?}, Content-Type: {:?}", filename, content_type);
match field.bytes().await {
Ok(bytes) => {
file_data = Some(bytes.to_vec());
break; // Process only the first file field
info!("Successfully read file data, size: {} bytes", file_data.as_ref().map_or(0, |d| d.len()));
},
Err(e) => {
log::error!("Failed to read file data: {}", e);
error!("Failed to read file data from multipart: {}", e);
return common_response(
StatusCode::BAD_REQUEST,
"Failed to read file data",
@@ -465,22 +469,20 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
}
}
"base64_data" => {
if file_data.is_some() {
warn!("Multiple file fields detected for profile upload. Only the first one will be processed.");
}
let base64_str = field.text().await.unwrap_or_default();
info!("Detected base64_data field, length: {}", base64_str.len());
if !base64_str.is_empty() {
match decode_base64_file(&base64_str) {
Ok(decoded_data) => {
file_data = Some(decoded_data);
// Extract content type from data URL if present
info!("Successfully decoded base64 data, size: {} bytes", file_data.as_ref().map_or(0, |d| d.len()));
if let Some(detected_type) = extract_content_type_from_data_url(&base64_str) {
content_type = Some(detected_type);
info!("Detected content type from base64 data URL: {}", content_type.as_ref().unwrap());
}
break; // Process only the first file field
}
Err(e) => {
log::error!("Failed to decode base64 data: {}", e);
error!("Failed to decode base64 data: {}", e);
return common_response(
StatusCode::BAD_REQUEST,
"Invalid base64 data",
@@ -491,57 +493,55 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
}
"filename" => {
filename = Some(field.text().await.unwrap_or_default());
info!("Received filename from field: {:?}", filename);
}
"content_type" => {
content_type = Some(field.text().await.unwrap_or_default());
info!("Received content_type from field: {:?}", content_type);
}
_ => {
// Skip unknown fields
info!("Skipping unknown multipart field: {}", name);
}
}
}
// If an old avatar exists, delete it from MinIO
if let Some(old_url) = old_avatar_url {
// Extract object path from the URL
// Assuming URL format is https://cdn.asepharyana.tech/{bucket_name}/{object_path}
let parts: Vec<&str> = old_url.splitn(4, '/').collect();
if parts.len() == 4 {
let old_object_path = parts[3];
log::info!("Deleting old avatar from MinIO: {}", old_object_path);
if let Err(e) = minio_service.delete_file(old_object_path).await {
log::error!("Failed to delete old avatar from MinIO: {}", e);
// Do not return error, continue with new upload
}
} else {
log::warn!("Could not parse old avatar URL for deletion: {}", old_url);
}
}
info!("Finished multipart form processing.");
// Validate required fields
let file_data = match file_data {
Some(data) => data,
None => {
error!("File data is missing after multipart processing.");
return common_response(
StatusCode::BAD_REQUEST,
"file data is required",
);
}
};
info!("File data extracted, size: {} bytes.", file_data.len());
let filename = filename.unwrap_or_else(|| "unnamed_file".to_string());
let content_type = content_type.unwrap_or_else(|| "application/octet-stream".to_string());
let filename = filename.unwrap_or_else(|| {
info!("Filename not provided, defaulting to 'unnamed_file'.");
"unnamed_file".to_string()
});
let content_type = content_type.unwrap_or_else(|| {
info!("Content type not provided, defaulting to 'application/octet-stream'.");
"application/octet-stream".to_string()
});
info!("Final filename: {}, Content-Type: {}", filename, content_type);
// Auto-detect file type based on content type and filename
let file_type = FileType::from_content_type(&content_type);
let file_type = if matches!(file_type, FileType::Unknown) {
info!("Content type detection failed, trying from filename.");
FileType::from_filename(&filename)
} else {
file_type
};
info!("Detected file type: {:?}", file_type);
// Validate file type is supported
if matches!(file_type, FileType::Unknown) {
error!("Unsupported file type detected: {:?}", file_type);
return common_response(
StatusCode::BAD_REQUEST,
"Unsupported file type. Supported types: JPEG, PNG, WEBP, GIF, PDF, DOC, DOCX",
@@ -550,20 +550,24 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
// Validate file type matches content type
if !file_type.allowed_types().contains(&content_type.as_str()) {
error!("File type '{:?}' does not match content type '{}'.", file_type, content_type);
return common_response(
StatusCode::BAD_REQUEST,
&format!("File type '{}' does not match content type '{:?}'", content_type, file_type),
&format!("File type '{:?}' does not match content type '{}'", file_type, content_type),
);
}
// Validate file size
if file_data.len() > file_type.max_size() {
error!("File too large. Current size: {} bytes, Max size for {:?}: {} bytes",
file_data.len(), file_type, file_type.max_size());
return common_response(
StatusCode::BAD_REQUEST,
&format!("File too large. Maximum size for {:?} is {} bytes",
file_type, file_type.max_size()),
);
}
info!("File size validated: {} bytes.", file_data.len());
// Create secure upload path with user ID (sanitized for filesystem)
let sanitized_user_id = user_email
@@ -571,21 +575,20 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
.replace(":", "_")
.replace("@", "_at_")
.replace(".", "_");
info!("Sanitized user ID for folder path: {}", sanitized_user_id);
let folder = format!("{}/{}", file_type.as_folder(), sanitized_user_id);
info!("Upload folder: {}", folder);
// Upload file to MinIO with deduplication
info!("Attempting to upload file to MinIO.");
match minio_service.upload_file_with_deduplication(&file_data, &content_type, &folder, &filename).await {
Ok(object_path) => {
info!("File uploaded successfully to MinIO. Object path: {}", object_path);
// Create permanent URL (no expiration)
let permanent_url = format!("https://cdn.asepharyana.tech/{}/{}",
bucket_name, object_path);
// Update user's avatar URL in the database
if let Err(e) = UsersService::update_user_avatar(&user_email, Some(permanent_url.clone()), &state).await {
log::error!("Failed to update user avatar in DB: {}", e);
// Continue with response, but log the error
}
info!("Permanent URL: {}", permanent_url);
let response_data = json!({
"filename": filename,
@@ -604,7 +607,7 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
})
}
Err(e) => {
log::error!("Failed to upload file: {}", e);
error!("Failed to upload file to MinIO: {}", e);
common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("Upload failed: {}", e),
+14 -10
View File
@@ -5,19 +5,23 @@ use tower_http::cors::CorsLayer;
pub fn cors_middleware() -> CorsLayer {
let env = &ENV;
let cors_origins = match env.rust_env.as_str() {
"development" => vec!["http://localhost:3000"],
"development" => {
let mut origins = vec!["http://localhost:3000".to_string()];
origins.push(format!("http://localhost:{}", env.port));
origins
},
"production" => {
vec![
"https://gacha.imphnen.dev",
"https://imphnen.dev",
"https://dimentorin.imphnen.dev",
"https://gacha.imphnen.dev".to_string(),
"https://imphnen.dev".to_string(),
"https://dimentorin.imphnen.dev".to_string(),
]
}
_ => vec![
"http://localhost:3000",
"https://gacha.imphnen.dev",
"https://imphnen.dev",
"https://dimentorin.imphnen.dev",
"http://localhost:3000".to_string(),
"https://gacha.imphnen.dev".to_string(),
"https://imphnen.dev".to_string(),
"https://dimentorin.imphnen.dev".to_string(),
],
};
let allowed_origins: Vec<HeaderValue> = cors_origins
@@ -27,7 +31,7 @@ pub fn cors_middleware() -> CorsLayer {
CorsLayer::new()
.allow_origin(allowed_origins)
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
.allow_credentials(true)
}
}