feat: Enhance CORS middleware to dynamically include development port and allow OPTIONS method
This commit is contained in:
@@ -18,8 +18,8 @@ use std::pin::Pin;
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use tracing::warn;
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use tracing::error;
|
||||||
use crate::v1::users::users_dto::{UsersDetailItemDto as UserDto, UsersCreateRequestDto as CreateUserDto};
|
use crate::v1::users::users_dto::{UsersDetailItemDto as UserDto, UsersCreateRequestDto as CreateUserDto};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -384,11 +384,16 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
|
|||||||
let state = state.to_owned();
|
let state = state.to_owned();
|
||||||
let user_id = user_id.to_owned();
|
let user_id = user_id.to_owned();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
info!("Entering upload_file function for user_id: {}", user_id);
|
||||||
|
|
||||||
// Initialize MinIO configuration
|
// Initialize MinIO configuration
|
||||||
let minio_config = match MinioConfig::from_env() {
|
let minio_config = match MinioConfig::from_env() {
|
||||||
Ok(config) => config,
|
Ok(config) => {
|
||||||
|
info!("MinIO config loaded successfully.");
|
||||||
|
config
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to load MinIO config: {}", e);
|
error!("Failed to load MinIO config: {}", e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"MinIO configuration error",
|
"MinIO configuration error",
|
||||||
@@ -398,12 +403,16 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
|
|||||||
|
|
||||||
// Store bucket name before minio_config is moved
|
// Store bucket name before minio_config is moved
|
||||||
let bucket_name = minio_config.bucket_name.clone();
|
let bucket_name = minio_config.bucket_name.clone();
|
||||||
|
info!("MinIO bucket name: {}", bucket_name);
|
||||||
|
|
||||||
// Initialize MinIO service
|
// Initialize MinIO service
|
||||||
let minio_service = match create_minio_service_from_config(minio_config).await {
|
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) => {
|
Err(e) => {
|
||||||
log::error!("Failed to initialize MinIO service: {}", e);
|
error!("Failed to initialize MinIO service: {}", e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"MinIO service initialization 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)
|
// Get actual user data from database using user_id (which is a UUID)
|
||||||
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
|
|
||||||
let repo = UsersRepository::new(&state);
|
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) => {
|
Ok(user) => {
|
||||||
// Extract the actual ID from the user record
|
info!("Found user in DB. User ID: {}, Email: {}", user.id.id.to_raw(), user.email);
|
||||||
let actual_id = user.id.id.to_raw();
|
user
|
||||||
let old_avatar = user.avatar.clone(); // Get the old avatar URL
|
|
||||||
(actual_id, user.email, old_avatar)
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
|
error!("Failed to find user in DB for ID {}: {}", user_id, e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"User 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 file_data: Option<Vec<u8>> = None;
|
||||||
let mut filename: Option<String> = None;
|
let mut filename: Option<String> = None;
|
||||||
let mut content_type: 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) {
|
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
||||||
let name = field.name().unwrap_or("").to_string();
|
let name = field.name().unwrap_or("").to_string();
|
||||||
|
info!("Processing multipart field: {}", name);
|
||||||
|
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
"file" => {
|
"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());
|
filename = field.file_name().map(|s| s.to_string());
|
||||||
content_type = field.content_type().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 {
|
match field.bytes().await {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
file_data = Some(bytes.to_vec());
|
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) => {
|
Err(e) => {
|
||||||
log::error!("Failed to read file data: {}", e);
|
error!("Failed to read file data from multipart: {}", e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"Failed to read file data",
|
"Failed to read file data",
|
||||||
@@ -465,22 +469,20 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"base64_data" => {
|
"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();
|
let base64_str = field.text().await.unwrap_or_default();
|
||||||
|
info!("Detected base64_data field, length: {}", base64_str.len());
|
||||||
if !base64_str.is_empty() {
|
if !base64_str.is_empty() {
|
||||||
match decode_base64_file(&base64_str) {
|
match decode_base64_file(&base64_str) {
|
||||||
Ok(decoded_data) => {
|
Ok(decoded_data) => {
|
||||||
file_data = Some(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) {
|
if let Some(detected_type) = extract_content_type_from_data_url(&base64_str) {
|
||||||
content_type = Some(detected_type);
|
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) => {
|
Err(e) => {
|
||||||
log::error!("Failed to decode base64 data: {}", e);
|
error!("Failed to decode base64 data: {}", e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"Invalid base64 data",
|
"Invalid base64 data",
|
||||||
@@ -491,57 +493,55 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
"filename" => {
|
"filename" => {
|
||||||
filename = Some(field.text().await.unwrap_or_default());
|
filename = Some(field.text().await.unwrap_or_default());
|
||||||
|
info!("Received filename from field: {:?}", filename);
|
||||||
}
|
}
|
||||||
"content_type" => {
|
"content_type" => {
|
||||||
content_type = Some(field.text().await.unwrap_or_default());
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!("Finished multipart form processing.");
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
let file_data = match file_data {
|
let file_data = match file_data {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => {
|
None => {
|
||||||
|
error!("File data is missing after multipart processing.");
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"file data is required",
|
"file data is required",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
info!("File data extracted, size: {} bytes.", file_data.len());
|
||||||
|
|
||||||
let filename = filename.unwrap_or_else(|| "unnamed_file".to_string());
|
let filename = filename.unwrap_or_else(|| {
|
||||||
let content_type = content_type.unwrap_or_else(|| "application/octet-stream".to_string());
|
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
|
// Auto-detect file type based on content type and filename
|
||||||
let file_type = FileType::from_content_type(&content_type);
|
let file_type = FileType::from_content_type(&content_type);
|
||||||
let file_type = if matches!(file_type, FileType::Unknown) {
|
let file_type = if matches!(file_type, FileType::Unknown) {
|
||||||
|
info!("Content type detection failed, trying from filename.");
|
||||||
FileType::from_filename(&filename)
|
FileType::from_filename(&filename)
|
||||||
} else {
|
} else {
|
||||||
file_type
|
file_type
|
||||||
};
|
};
|
||||||
|
info!("Detected file type: {:?}", file_type);
|
||||||
|
|
||||||
// Validate file type is supported
|
// Validate file type is supported
|
||||||
if matches!(file_type, FileType::Unknown) {
|
if matches!(file_type, FileType::Unknown) {
|
||||||
|
error!("Unsupported file type detected: {:?}", file_type);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"Unsupported file type. Supported types: JPEG, PNG, WEBP, GIF, PDF, DOC, DOCX",
|
"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
|
// Validate file type matches content type
|
||||||
if !file_type.allowed_types().contains(&content_type.as_str()) {
|
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(
|
return common_response(
|
||||||
StatusCode::BAD_REQUEST,
|
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
|
// Validate file size
|
||||||
if file_data.len() > file_type.max_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(
|
return common_response(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
&format!("File too large. Maximum size for {:?} is {} bytes",
|
&format!("File too large. Maximum size for {:?} is {} bytes",
|
||||||
file_type, file_type.max_size()),
|
file_type, file_type.max_size()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
info!("File size validated: {} bytes.", file_data.len());
|
||||||
|
|
||||||
// Create secure upload path with user ID (sanitized for filesystem)
|
// Create secure upload path with user ID (sanitized for filesystem)
|
||||||
let sanitized_user_id = user_email
|
let sanitized_user_id = user_email
|
||||||
@@ -571,21 +575,20 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
|
|||||||
.replace(":", "_")
|
.replace(":", "_")
|
||||||
.replace("@", "_at_")
|
.replace("@", "_at_")
|
||||||
.replace(".", "_");
|
.replace(".", "_");
|
||||||
|
info!("Sanitized user ID for folder path: {}", sanitized_user_id);
|
||||||
|
|
||||||
let folder = format!("{}/{}", file_type.as_folder(), sanitized_user_id);
|
let folder = format!("{}/{}", file_type.as_folder(), sanitized_user_id);
|
||||||
|
info!("Upload folder: {}", folder);
|
||||||
|
|
||||||
// Upload file to MinIO with deduplication
|
// 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 {
|
match minio_service.upload_file_with_deduplication(&file_data, &content_type, &folder, &filename).await {
|
||||||
Ok(object_path) => {
|
Ok(object_path) => {
|
||||||
|
info!("File uploaded successfully to MinIO. Object path: {}", object_path);
|
||||||
// Create permanent URL (no expiration)
|
// Create permanent URL (no expiration)
|
||||||
let permanent_url = format!("https://cdn.asepharyana.tech/{}/{}",
|
let permanent_url = format!("https://cdn.asepharyana.tech/{}/{}",
|
||||||
bucket_name, object_path);
|
bucket_name, object_path);
|
||||||
|
info!("Permanent URL: {}", permanent_url);
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
let response_data = json!({
|
let response_data = json!({
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
@@ -604,7 +607,7 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to upload file: {}", e);
|
error!("Failed to upload file to MinIO: {}", e);
|
||||||
common_response(
|
common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
&format!("Upload failed: {}", e),
|
&format!("Upload failed: {}", e),
|
||||||
|
|||||||
@@ -5,19 +5,23 @@ use tower_http::cors::CorsLayer;
|
|||||||
pub fn cors_middleware() -> CorsLayer {
|
pub fn cors_middleware() -> CorsLayer {
|
||||||
let env = &ENV;
|
let env = &ENV;
|
||||||
let cors_origins = match env.rust_env.as_str() {
|
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" => {
|
"production" => {
|
||||||
vec![
|
vec![
|
||||||
"https://gacha.imphnen.dev",
|
"https://gacha.imphnen.dev".to_string(),
|
||||||
"https://imphnen.dev",
|
"https://imphnen.dev".to_string(),
|
||||||
"https://dimentorin.imphnen.dev",
|
"https://dimentorin.imphnen.dev".to_string(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
_ => vec![
|
_ => vec![
|
||||||
"http://localhost:3000",
|
"http://localhost:3000".to_string(),
|
||||||
"https://gacha.imphnen.dev",
|
"https://gacha.imphnen.dev".to_string(),
|
||||||
"https://imphnen.dev",
|
"https://imphnen.dev".to_string(),
|
||||||
"https://dimentorin.imphnen.dev",
|
"https://dimentorin.imphnen.dev".to_string(),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
let allowed_origins: Vec<HeaderValue> = cors_origins
|
let allowed_origins: Vec<HeaderValue> = cors_origins
|
||||||
@@ -27,7 +31,7 @@ pub fn cors_middleware() -> CorsLayer {
|
|||||||
|
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_origin(allowed_origins)
|
.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_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
|
||||||
.allow_credentials(true)
|
.allow_credentials(true)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user