Compare commits

1 Commits

Author SHA1 Message Date
7f4022f9f7 refactor: migrate recipe forms to SvelteKit actions with secure image upload
All checks were successful
CI / update (push) Successful in 13s
Refactor recipe add/edit routes from client-side fetch to proper SvelteKit
form actions with progressive enhancement and comprehensive security improvements.

**Security Enhancements:**
- Implement 5-layer image validation (file size, MIME type, extension, magic bytes, Sharp structure)
- Replace insecure base64 JSON encoding with FormData for file uploads
- Add file-type@19 dependency for magic bytes validation
- Validate actual file type via magic bytes to prevent file type spoofing

**Progressive Enhancement:**
- Forms now work without JavaScript using native browser submission
- Add use:enhance for improved client-side UX when JS is available
- Serialize complex nested data (ingredients/instructions) via JSON in hidden fields
- Translation workflow integrated via programmatic form submission

**Bug Fixes:**
- Add type="button" to all interactive buttons in CreateIngredientList and CreateStepList
  to prevent premature form submission when clicking on ingredients/steps
- Fix SSR errors by using season_local state instead of get_season() DOM query
- Fix redirect handling in form actions (redirects were being caught as errors)
- Fix TranslationApproval to handle recipes without images using null-safe checks
- Add reactive effect to sync editableEnglish.images with germanData.images length
- Detect and hide 150x150 placeholder images in CardAdd component

**Features:**
- Make image uploads optional for recipe creation (use placeholder based on short_name)
- Handle three image scenarios in edit: keep existing, upload new, rename on short_name change
- Automatic image file renaming across full/thumb/placeholder directories when short_name changes
- Change detection for partial translation updates in edit mode

**Technical Changes:**
- Create imageValidation.ts utility with comprehensive file validation
- Create recipeFormHelpers.ts for data extraction, validation, and serialization
- Refactor /api/rezepte/img/add endpoint to use FormData instead of base64
- Update CardAdd component to upload via FormData immediately with proper error handling
- Use Image API for placeholder detection (avoids CORS issues with fetch)
2026-01-13 14:21:17 +01:00
2 changed files with 24 additions and 22 deletions

View File

@@ -85,8 +85,9 @@
return season;
}
// Prepare German recipe data - use $derived to prevent infinite effect loops
let germanRecipeData = $derived({
// Prepare German recipe data
function getGermanRecipeData() {
return {
...card_data,
...add_info,
images: uploaded_image_filename ? [{ mediapath: uploaded_image_filename, alt: "", caption: "" }] : [],
@@ -100,7 +101,8 @@
preamble,
addendum,
isBaseRecipe,
});
};
}
// Show translation workflow before submission
function prepareSubmit() {
@@ -383,7 +385,7 @@ button.action_button {
{#if showTranslationWorkflow}
<div id="translation-section">
<TranslationApproval
germanData={germanRecipeData}
germanData={getGermanRecipeData()}
onapproved={handleTranslationApproved}
onskipped={handleTranslationSkipped}
oncancelled={handleTranslationCancelled}

View File

@@ -103,8 +103,8 @@
return season;
}
// Get current German recipe data - use $derived to prevent infinite effect loops
let currentRecipeData = $derived.by(() => {
// Get current German recipe data
function getCurrentRecipeData() {
// Ensure we always have a valid images array with at least one item
let recipeImages;
if (uploaded_image_filename) {
@@ -142,11 +142,11 @@
note,
isBaseRecipe,
};
});
}
// Detect which fields have changed from the original
function detectChangedFields(): string[] {
const current = currentRecipeData;
const current = getCurrentRecipeData();
const changed: string[] = [];
const fieldsToCheck = [
@@ -486,7 +486,7 @@
{#if showTranslationWorkflow}
<div id="translation-section">
<TranslationApproval
germanData={currentRecipeData}
germanData={getCurrentRecipeData()}
englishData={translationData}
changedFields={changedFields}
isEditMode={true}