Compare commits

...

2 Commits

Author SHA1 Message Date
Aiirondev_dev c62b2b553d improvements in processing the library item uploading
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-16 22:50:17 +02:00
Aiirondev_dev 8783f97a09 improvements in displaying the Images in the detailed view
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 22:19:41 +02:00
3 changed files with 130 additions and 161 deletions
+27 -23
View File
@@ -3828,7 +3828,10 @@ def api_item_detail(item_id):
""" """
client.close() client.close()
return detail_html, 200 return jsonify({
'html': detail_html,
'images': item.get('Images', item.get('Bilder', []))
}), 200
except Exception as e: except Exception as e:
app.logger.error(f"Error fetching item detail: {e}") app.logger.error(f"Error fetching item detail: {e}")
return jsonify({'error': 'An error occurred while fetching the item detail'}), 500 return jsonify({'error': 'An error occurred while fetching the item detail'}), 500
@@ -9958,18 +9961,21 @@ def fetch_book_info(isbn):
app.logger.error(f"Error fetching book data: {e}") app.logger.error(f"Error fetching book data: {e}")
return jsonify({"error": f"Failed to fetch book information"}), 500 return jsonify({"error": f"Failed to fetch book information"}), 500
@app.route('/download_book_cover', methods=['POST']) @app.route('/download_book_cover', methods=['POST'])
def download_book_cover(): def download_book_cover():
""" """
API endpoint to download and save a book cover image from URL API endpoint to download a book cover image from URL
and save it directly to MongoDB GridFS.
""" """
if 'username' not in session: if 'username' not in session:
return jsonify({"error": "Not authorized"}), 403 return jsonify({"error": "Not authorized"}), 403
current_permissions = us.get_effective_permissions(session['username']) current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_insert', False): if not current_permissions['actions'].get('can_insert', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') return jsonify({"error": "Ihnen fehlen die nötigen Berechtigungen."}), 403
return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403 return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
@@ -9984,17 +9990,11 @@ def download_book_cover():
if parsed_url.scheme != 'https' or not parsed_url.netloc: if parsed_url.scheme != 'https' or not parsed_url.netloc:
return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400 return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400
# 2. SSRF Protection: Strict Allowlist Check
# if parsed_url.netloc not in ALLOWED_COVER_DOMAINS:
# return jsonify({"error": "Target host is not an allowed book cover provider"}), 403
# Download the image (allow_redirects=False prevents redirecting to internal IPs)
response = requests.get(image_url, stream=True, timeout=10, allow_redirects=False) response = requests.get(image_url, stream=True, timeout=10, allow_redirects=False)
if response.status_code != 200: if response.status_code != 200:
return jsonify({"error": f"Failed to download image: Status {response.status_code}"}), 400 return jsonify({"error": f"Failed to download image: Status {response.status_code}"}), 400
# Check content type
content_type = response.headers.get('content-type', '') content_type = response.headers.get('content-type', '')
allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'] allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
@@ -10003,16 +10003,14 @@ def download_book_cover():
"error": f"Nicht unterstütztes Bildformat: {content_type}. Erlaubte Formate: JPG, JPEG, PNG, GIF" "error": f"Nicht unterstütztes Bildformat: {content_type}. Erlaubte Formate: JPG, JPEG, PNG, GIF"
}), 400 }), 400
# Check content length header
content_length = response.headers.get('Content-Length') content_length = response.headers.get('Content-Length')
if content_length: if content_length:
try: try:
if int(content_length) > 5 * 1024 * 1024: if int(content_length) > 5 * 1024 * 1024:
return jsonify({"error": "Image is too large"}), 413 return jsonify({"error": "Image is too large (max 5MB)"}), 413
except ValueError: except ValueError:
pass pass
# Generate a fully unique filename
unique_id = str(uuid.uuid4()) unique_id = str(uuid.uuid4())
timestamp = time.strftime("%Y%m%d%H%M%S") timestamp = time.strftime("%Y%m%d%H%M%S")
@@ -10023,23 +10021,29 @@ def download_book_cover():
extension = '.gif' extension = '.gif'
filename = f"book_cover_{unique_id}_{timestamp}{extension}" filename = f"book_cover_{unique_id}_{timestamp}{extension}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# Save image in chunks (prevents memory exhaustion and enforces size limits) image_data = io.BytesIO()
with open(filepath, 'wb') as f:
written = 0 written = 0
for chunk in response.iter_content(chunk_size=8192): for chunk in response.iter_content(chunk_size=8192):
written += len(chunk) written += len(chunk)
if written > 5 * 1024 * 1024: if written > 5 * 1024 * 1024:
# Clean up the partial file before aborting return jsonify({"error": "Image is too large (max 5MB)"}), 413
os.remove(filepath) image_data.write(chunk)
return jsonify({"error": "Image is too large"}), 413
f.write(chunk) image_data.seek(0)
fs = get_gridfs()
fs.put(
image_data,
filename=filename,
content_type=content_type
)
return jsonify({ return jsonify({
"success": True, "success": True,
"filename": filename, "filename": filename,
"message": "Image downloaded successfully" "message": "Image downloaded and stored directly in database"
}) })
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
@@ -10047,8 +10051,8 @@ def download_book_cover():
return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500 return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500
except Exception as e: except Exception as e:
app.logger.error(f"Error downloading book cover: {e}") app.logger.error(f"Error downloading book cover: {e}")
# Fixed syntax here: Removed the injected HTML that was appended to this line return jsonify({"error": "Failed to download image"}), 500
return jsonify({"error": f"Failed to download image"}), 500
""" """
@app.route('/proxy_image') @app.route('/proxy_image')
def proxy_image(): def proxy_image():
+13 -35
View File
@@ -1331,51 +1331,29 @@
detailContent.innerHTML = '<p>Lade Details...</p>'; detailContent.innerHTML = '<p>Lade Details...</p>';
detailModal.style.display = 'flex'; detailModal.style.display = 'flex';
// Sicherer Zugriff auf das Item mit Fallback
const item = libraryItems.find(i => i._id === itemId); const item = libraryItems.find(i => i._id === itemId);
let mediaHtml = ''; let mediaHtml = '';
// Robuste Prüfung: Wir testen gängige Benennungen aus deinem Backend if (item) {
// Prüfe gängige Array-Namen aus dem Backend
const imageArray = item.Images || item.Bilder || item.images; const imageArray = item.Images || item.Bilder || item.images;
if (item && Array.isArray(imageArray) && imageArray.length > 0) { if (Array.isArray(imageArray) && imageArray.length > 0) {
const imagesHtml = imageArray.map((image, index) => { const imagesHtml = imageArray.map(image => {
// Direkter, robuster Pfad zur Upload-Route
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http')
? image
: `/uploads/${image}`;
// Dein neuer Code für die exakte Routen-Generierung return `<img src="${imageSrc}" alt="${escapeHtml(item.Name || 'Medium')}" class="item-image" style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #ddd; margin-right: 15px; margin-bottom: 15px;">`;
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http') ?
image :
`{{ url_for('uploaded_file', filename='') }}${image}`;
const thumbnailInfo = item.ThumbnailInfo && item.ThumbnailInfo[index];
const isVideo = isVideoFile(image);
if (isVideo) {
const videoSrc = thumbnailInfo && thumbnailInfo.has_thumbnail
? thumbnailInfo.thumbnail_url
: imageSrc;
if (thumbnailInfo && thumbnailInfo.has_thumbnail) {
return `
<div class="video-container" style="position: relative; width: 120px; height: 120px; display: inline-block; margin-right: 15px; margin-bottom: 15px;">
<img src="${videoSrc}" alt="${escapeHtml(item.Name || 'Medium')}" class="item-image" style="width: 100%; height: 100%; object-fit: cover; border-radius: 8px; border: 1px solid #ddd;">
<div class="video-preview-overlay" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; background: rgba(0,0,0,0.6); border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font-size: 16px;">
</div>
</div>`;
} else {
return `<div style="width: 120px; height: 120px; background: #333; color: #fff; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; margin-right: 15px; margin-bottom: 15px;">VIDEO</div>`;
}
} else {
const imageSrcFinal = thumbnailInfo && thumbnailInfo.has_thumbnail
? thumbnailInfo.thumbnail_url
: imageSrc;
return `<img src="${imageSrcFinal}" alt="${escapeHtml(item.Name || 'Medium')}" class="item-image" style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #ddd; margin-right: 15px; margin-bottom: 15px;">`;
}
}).join(''); }).join('');
mediaHtml = `<div class="detail-gallery-container" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap;">${imagesHtml}</div>`; mediaHtml = `<div class="detail-gallery-container" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap;">${imagesHtml}</div>`;
} }
}
// Zusätzliche Details vom Backend laden
fetch(`/api/item_detail/${itemId}`) fetch(`/api/item_detail/${itemId}`)
.then(response => { .then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
@@ -1390,7 +1368,7 @@
}); });
} }
// Closes the modal via the 'x' button // Schließt das Modal über den 'x'-Button
function closeDetailModal() { function closeDetailModal() {
document.getElementById('detailModal').style.display = 'none'; document.getElementById('detailModal').style.display = 'none';
} }
+42 -55
View File
@@ -876,12 +876,14 @@
</div> </div>
<!-- Image upload -->
<div class="form-group"> <div class="form-group">
<label for="images">Bilder:</label> <label>Buchcover (automatisch):</label>
<div id="book-cover-preview-container"></div>
</div>
<div class="form-group">
<label for="images"> Bilder hinzufügen:</label>
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif" multiple> <input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif" multiple>
<div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF</div> <div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF</div>
<!-- Add image preview area -->
<div class="image-preview-container" id="image-preview-container"></div> <div class="image-preview-container" id="image-preview-container"></div>
</div> </div>
@@ -1669,23 +1671,24 @@
}, 3000); }, 3000);
} }
// Function to download book cover image
function downloadBookCover(imageUrl) { function downloadBookCover(imageUrl) {
if (!imageUrl) { if (!imageUrl) {
console.log('No image URL provided'); console.log('No image URL provided');
return; return;
} }
// Show loading indicator for image download const coverPreviewContainer = document.getElementById('book-cover-preview-container');
const imagePreviewContainer = document.getElementById('image-preview-container');
if (imagePreviewContainer) { if (!coverPreviewContainer) {
console.error('Error: "book-cover-preview-container" not found in the DOM.');
return;
}
const loadingDiv = document.createElement('div'); const loadingDiv = document.createElement('div');
loadingDiv.className = 'image-loading'; loadingDiv.className = 'image-loading';
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>'; loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
imagePreviewContainer.appendChild(loadingDiv); coverPreviewContainer.appendChild(loadingDiv);
}
// Download the image via backend
fetch('/download_book_cover', { fetch('/download_book_cover', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -1695,76 +1698,61 @@
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Remove loading indicator const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading'); if (currentLoadingDiv) {
if (loadingDiv) { currentLoadingDiv.remove();
loadingDiv.remove();
} }
if (data.success) { if (data.success) {
// Create a preview of the downloaded image coverPreviewContainer.innerHTML = '';
const imagePreview = document.createElement('div'); const imagePreview = document.createElement('div');
imagePreview.className = 'book-cover-preview'; imagePreview.className = 'book-cover-preview';
imagePreview.innerHTML = ` imagePreview.innerHTML = `
<div class="preview-item"> <div class="preview-item">
<img src="{{ url_for('uploaded_file', filename='') }}${data.filename}" <img src="/uploads/${data.filename}"
alt="Buchcover" class="book-cover-thumbnail"> alt="Buchcover" class="book-cover-thumbnail" style="max-width: 150px; border-radius: 4px;">
<p class="book-cover-caption">Buchcover automatisch heruntergeladen</p> <p class="book-cover-caption" style="font-size: 0.9em; color: #555;">Buchcover automatisch heruntergeladen</p>
<input type="hidden" name="book_cover_image" value="${data.filename}"> <input type="hidden" name="book_cover_image" value="${data.filename}">
<button type="button" onclick="removeBookCover(this)" <button type="button" onclick="removeBookCover(this)"
class="remove-book-cover-button"> class="remove-book-cover-button btn btn-sm btn-danger">
Entfernen Entfernen
</button> </button>
</div> </div>
`; `;
if (imagePreviewContainer) { coverPreviewContainer.appendChild(imagePreview);
imagePreviewContainer.appendChild(imagePreview);
}
console.log('Book cover downloaded successfully:', data.filename); console.log('Book cover downloaded successfully:', data.filename);
} else { } else {
console.error('Failed to download book cover:', data.error); console.error('Failed to download book cover:', data.error);
// Show error message to user showCoverError(coverPreviewContainer, 'Fehler beim Herunterladen des Buchcovers: ' + data.error);
if (imagePreviewContainer) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = 'Fehler beim Herunterladen des Buchcovers: ' + data.error;
errorDiv.style.fontSize = '0.8em';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
imagePreviewContainer.appendChild(errorDiv);
// Remove error message after 5 seconds
setTimeout(() => errorDiv.remove(), 5000);
}
} }
}) })
.catch(error => { .catch(error => {
console.error('Error downloading book cover:', error); console.error('Error downloading book cover:', error);
// Remove loading indicator
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading'); const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
if (loadingDiv) { if (currentLoadingDiv) {
loadingDiv.remove(); currentLoadingDiv.remove();
} }
// Show error message showCoverError(coverPreviewContainer, 'Netzwerkfehler beim Herunterladen des Buchcovers');
if (imagePreviewContainer) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = 'Netzwerkfehler beim Herunterladen des Buchcovers';
errorDiv.style.fontSize = '0.8em';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
imagePreviewContainer.appendChild(errorDiv);
// Remove error message after 5 seconds
setTimeout(() => errorDiv.remove(), 5000);
}
}); });
} }
// Function to remove downloaded book cover function showCoverError(container, message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
errorDiv.style.fontSize = '0.8em';
errorDiv.style.color = 'red';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
container.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
function removeBookCover(button) { function removeBookCover(button) {
const previewItem = button.closest('.preview-item'); const previewItem = button.closest('.preview-item');
if (previewItem) { if (previewItem) {
@@ -1772,7 +1760,6 @@
} }
} }
// Code validation functions
function checkCodeUnique(code, excludeId, callback) { function checkCodeUnique(code, excludeId, callback) {
if (!code || code.trim() === '') { if (!code || code.trim() === '') {
callback(true); callback(true);