church-api/webp_submit.sh
Benjamin Slingo 0c06e159bb Initial commit: Church API Rust implementation
Complete church management system with bulletin management, media processing, live streaming integration, and web interface. Includes authentication, email notifications, database migrations, and comprehensive test suite.
2025-08-19 20:56:41 -04:00

311 lines
12 KiB
Bash
Executable file

#!/bin/bash
echo "🔄 UPDATING EVENTS HANDLER (CORRECT VERSION)"
echo "============================================="
# Step 1: Restore from backup if needed
if [ -f src/handlers/events.rs.backup.* ]; then
echo "📦 Restoring from backup first..."
cp src/handlers/events.rs.backup.* src/handlers/events.rs
echo "✅ Restored"
fi
# Step 2: Add the correct imports
echo "📝 Adding correct imports..."
sed -i '/^use uuid::Uuid;$/a\
\
// New imports for WebP and multipart support\
use axum::extract::Multipart;\
use crate::utils::images::convert_to_webp;\
use tokio::fs;\
use chrono::{DateTime, Utc};' src/handlers/events.rs
echo "✅ Imports added"
# Step 3: Create the CORRECT submit function
echo "🔧 Creating CORRECT submit function..."
cat > /tmp/correct_submit_function.rs << 'EOF'
pub async fn submit(
State(state): State<AppState>,
mut multipart: Multipart,
) -> Result<Json<ApiResponse<PendingEvent>>> {
// Initialize the request struct with ACTUAL fields
let mut req = SubmitEventRequest {
title: String::new(),
description: String::new(),
start_time: Utc::now(), // Temporary default
end_time: Utc::now(), // Temporary default
location: String::new(),
location_url: None,
category: String::new(),
is_featured: None,
recurring_type: None,
bulletin_week: String::new(),
submitter_email: None,
};
// Track image paths (we'll save these separately to DB)
let mut image_path: Option<String> = None;
let mut thumbnail_path: Option<String> = None;
// Extract form fields and files
while let Some(field) = multipart.next_field().await.map_err(|e| {
ApiError::ValidationError(format!("Failed to read multipart field: {}", e))
})? {
let name = field.name().unwrap_or("").to_string();
match name.as_str() {
"title" => {
req.title = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid title: {}", e))
})?;
},
"description" => {
req.description = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid description: {}", e))
})?;
},
"start_time" => {
let time_str = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid start_time: {}", e))
})?;
// Parse as NaiveDateTime first, then convert to UTC
let naive_dt = chrono::NaiveDateTime::parse_from_str(&time_str, "%Y-%m-%dT%H:%M")
.map_err(|e| ApiError::ValidationError(format!("Invalid start_time format: {}", e)))?;
req.start_time = DateTime::from_utc(naive_dt, Utc);
},
"end_time" => {
let time_str = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid end_time: {}", e))
})?;
let naive_dt = chrono::NaiveDateTime::parse_from_str(&time_str, "%Y-%m-%dT%H:%M")
.map_err(|e| ApiError::ValidationError(format!("Invalid end_time format: {}", e)))?;
req.end_time = DateTime::from_utc(naive_dt, Utc);
},
"location" => {
req.location = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid location: {}", e))
})?;
},
"category" => {
req.category = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid category: {}", e))
})?;
},
"location_url" => {
let url = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid location_url: {}", e))
})?;
if !url.is_empty() {
req.location_url = Some(url);
}
},
"reoccuring" => { // Note: form uses "reoccuring" but model uses "recurring_type"
let recurring = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid recurring: {}", e))
})?;
if !recurring.is_empty() {
req.recurring_type = Some(recurring);
}
},
"submitter_email" => {
let email = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid submitter_email: {}", e))
})?;
if !email.is_empty() {
req.submitter_email = Some(email);
}
},
"bulletin_week" => {
req.bulletin_week = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid bulletin_week: {}", e))
})?;
},
"image" => {
let image_data = field.bytes().await.map_err(|e| {
ApiError::ValidationError(format!("Failed to read image: {}", e))
})?;
if !image_data.is_empty() {
// Save original immediately
let uuid = Uuid::new_v4();
let original_path = format!("uploads/events/original_{}.jpg", uuid);
// Ensure directory exists
fs::create_dir_all("uploads/events").await.map_err(|e| {
ApiError::FileError(e)
})?;
fs::write(&original_path, &image_data).await.map_err(|e| {
ApiError::FileError(e)
})?;
// Set original path immediately
image_path = Some(original_path.clone());
// Convert to WebP in background (user doesn't wait)
let pool = state.pool.clone();
tokio::spawn(async move {
if let Ok(webp_data) = convert_to_webp(&image_data).await {
let webp_path = format!("uploads/events/{}.webp", uuid);
if fs::write(&webp_path, webp_data).await.is_ok() {
// Update database with WebP path (using actual column name "image")
let _ = sqlx::query!(
"UPDATE pending_events SET image = $1 WHERE image = $2",
webp_path,
original_path
).execute(&pool).await;
// Delete original file
let _ = fs::remove_file(&original_path).await;
}
}
});
}
},
"thumbnail" => {
let thumb_data = field.bytes().await.map_err(|e| {
ApiError::ValidationError(format!("Failed to read thumbnail: {}", e))
})?;
if !thumb_data.is_empty() {
let uuid = Uuid::new_v4();
let original_path = format!("uploads/events/thumb_original_{}.jpg", uuid);
fs::create_dir_all("uploads/events").await.map_err(|e| {
ApiError::FileError(e)
})?;
fs::write(&original_path, &thumb_data).await.map_err(|e| {
ApiError::FileError(e)
})?;
thumbnail_path = Some(original_path.clone());
// Convert thumbnail to WebP in background
let pool = state.pool.clone();
tokio::spawn(async move {
if let Ok(webp_data) = convert_to_webp(&thumb_data).await {
let webp_path = format!("uploads/events/thumb_{}.webp", uuid);
if fs::write(&webp_path, webp_data).await.is_ok() {
let _ = sqlx::query!(
"UPDATE pending_events SET thumbnail = $1 WHERE thumbnail = $2",
webp_path,
original_path
).execute(&pool).await;
let _ = fs::remove_file(&original_path).await;
}
}
});
}
},
_ => {
// Ignore unknown fields
let _ = field.bytes().await;
}
}
}
// Validate required fields
if req.title.is_empty() {
return Err(ApiError::ValidationError("Title is required".to_string()));
}
if req.description.is_empty() {
return Err(ApiError::ValidationError("Description is required".to_string()));
}
if req.location.is_empty() {
return Err(ApiError::ValidationError("Location is required".to_string()));
}
if req.category.is_empty() {
return Err(ApiError::ValidationError("Category is required".to_string()));
}
if req.bulletin_week.is_empty() {
req.bulletin_week = "current".to_string(); // Default value
}
// Submit to database first
let mut pending_event = db::events::submit_for_approval(&state.pool, req).await?;
// Update with image paths if we have them
if let Some(img_path) = image_path {
sqlx::query!(
"UPDATE pending_events SET image = $1 WHERE id = $2",
img_path,
pending_event.id
).execute(&state.pool).await.map_err(ApiError::DatabaseError)?;
}
if let Some(thumb_path) = thumbnail_path {
sqlx::query!(
"UPDATE pending_events SET thumbnail = $1 WHERE id = $2",
thumb_path,
pending_event.id
).execute(&state.pool).await.map_err(ApiError::DatabaseError)?;
}
// Send email notification to admin (existing logic)
let mailer = state.mailer.clone();
let event_for_email = pending_event.clone();
tokio::spawn(async move {
if let Err(e) = mailer.send_event_submission_notification(&event_for_email).await {
tracing::error!("Failed to send email: {:?}", e);
} else {
tracing::info!("Email sent for event: {}", event_for_email.title);
}
});
Ok(Json(ApiResponse {
success: true,
data: Some(pending_event),
message: Some("Event submitted successfully! Images are being optimized in the background.".to_string()),
}))
}
EOF
# Step 4: Replace the old submit function with the CORRECT one
echo "🔄 Replacing submit function..."
# Find the line numbers of the current submit function
start_line=$(grep -n "^pub async fn submit(" src/handlers/events.rs | cut -d: -f1)
end_line=$(awk "NR>$start_line && /^}/ {print NR; exit}" src/handlers/events.rs)
if [ -n "$start_line" ] && [ -n "$end_line" ]; then
# Create a temporary file with everything except the old submit function
head -n $((start_line - 1)) src/handlers/events.rs > /tmp/events_before.rs
tail -n +$((end_line + 1)) src/handlers/events.rs > /tmp/events_after.rs
# Combine: before + new function + after
cat /tmp/events_before.rs /tmp/correct_submit_function.rs /tmp/events_after.rs > src/handlers/events.rs
echo "✅ Submit function replaced successfully"
else
echo "❌ Could not find submit function boundaries"
exit 1
fi
# Step 5: Clean up temp files
rm -f /tmp/correct_submit_function.rs /tmp/events_before.rs /tmp/events_after.rs
# Step 6: Build to check for errors
echo "🔨 Building to verify changes..."
if cargo build; then
echo "✅ Build successful!"
echo ""
echo "🎉 EVENTS HANDLER UPDATED SUCCESSFULLY!"
echo "======================================"
echo "✅ Uses ACTUAL model fields"
echo "✅ Proper DateTime handling"
echo "✅ Correct ApiError variants"
echo "✅ Real database columns"
echo "✅ Background WebP conversion"
echo "✅ No hallucinated bullshit"
echo ""
echo "🚀 Ready to handle multipart form submissions with WebP conversion!"
else
echo "❌ Build failed - check errors above"
exit 1
fi