Compare commits

...

25 Commits

Author SHA1 Message Date
Aiirondev_dev ea73b98549 Adjustments to the edeting funktion fpo 2026-06-28 17:03:10 +02:00
Aiirondev_dev d6dda0f25a Slight adjustments 2026-06-28 16:55:57 +02:00
Aiirondev_dev 46176c1741 slight fixes 2026-06-28 16:45:53 +02:00
Aiirondev_dev c57b4a9a44 edit Modal changes to accomidate the library Modul 2026-06-28 16:30:41 +02:00
Aiirondev_dev 6f424cc8aa Backup and restore funktionality 2026-06-28 15:55:49 +02:00
Aiirondev_dev 103b82af15 smal fix 2026-06-28 00:11:16 +02:00
Aiirondev_dev 69c8aa8700 Development fix 2026-06-27 23:59:56 +02:00
Aiirondev_dev 1fa3dace17 changes for a smal issue 2026-06-27 23:51:47 +02:00
Aiirondev_dev 6dced8b1a5 additional fix for the cathergorie 2026-06-27 23:33:01 +02:00
Aiirondev_dev 493ee2ea7f fiy of a funktion issue 2026-06-27 23:28:00 +02:00
Aiirondev_dev 52a2ec0cad Adding of the detailed module 2026-06-27 23:10:14 +02:00
Aiirondev_dev b5f2c3c5c5 Adding of the category 2026-06-27 23:09:24 +02:00
Aiirondev_dev 403c281c93 Debug? 2026-06-27 22:52:40 +02:00
Aiirondev_dev 6d121f6110 Slight adjustment 2026-06-27 22:37:04 +02:00
Aiirondev_dev 9d940c6151 Denbugging and fix of a Code_4 generation issue 2026-06-27 22:20:10 +02:00
Aiirondev_dev 980b825e07 Test 2026-06-27 22:11:28 +02:00
Aiirondev_dev 94326728eb development debuggig 2026-06-27 22:01:51 +02:00
Aiirondev_dev efe17883a6 Development version 2026-06-27 21:51:50 +02:00
Aiirondev_dev 76f93b3d2e Fix of an unused detail in the Detail Modal 2026-06-27 21:18:51 +02:00
Aiirondev_dev 8f793045c2 removed unused return information 2026-06-27 17:24:42 +02:00
Aiirondev_dev a199439d76 slight fix 2026-06-27 17:20:46 +02:00
Aiirondev_dev 6cec483390 slight change to the mongodb accesing 2026-06-27 17:08:59 +02:00
Aiirondev_dev 10e2f7cc17 fur5ther fixes of to much information getting out 2026-06-27 17:00:40 +02:00
Aiirondev_dev 0f8c5a0fec slight adjustments for the security 2026-06-27 16:29:13 +02:00
Aiirondev_dev 091147ecfd slight security fixes to prevent malisios actors from getting access or retrieving unfit information 2026-06-27 16:07:26 +02:00
5 changed files with 504 additions and 170 deletions
+147 -123
View File
@@ -388,7 +388,7 @@ ALLOWED_COVER_DOMAINS = {
"www.googleapis.com"
}
SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone"]
SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone", "borrower", "ip"]
# Apply the configuration for general use throughout the app
APP_VERSION = __version__
@@ -660,7 +660,7 @@ def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
return jsonify({'error': message}), 400
flash(message, 'error')
return redirect(request.referrer or url_for('home'))
return redirect(url_for('login'))
def _get_current_module(path):
"""Resolve the active UI module for navbar separation."""
@@ -1635,7 +1635,7 @@ def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
file_content.seek(0) # Reset file pointer after reading
except Exception as e:
error_msg = f"Error validating image content for {filename}: {str(e)}"
error_msg = f"Error validating image content for {filename}"
app.logger.error(error_msg)
if extension == 'png':
@@ -1675,9 +1675,9 @@ def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
except Exception as raw_err:
app.logger.error(f"PNG DEBUG: Error during raw file analysis: {str(raw_err)}")
traceback.print_exc()
return False, f"Datei '{filename}' konnte nicht als Bild erkannt werden. Fehler: {str(e)}"
return False, f"Datei '{filename}' konnte nicht als Bild erkannt werden. "
# Add more content type validations as needed for other file types
@@ -2835,8 +2835,8 @@ def catch_all_files(filename):
# If we get here, the file wasn't found
return Response(f"File {filename} not found", status=404)
except Exception as e:
print(f"Error in catch-all route for {filename}: {str(e)}")
return Response(f"Error serving file: {str(e)}", status=500)
app.logger.error(f"Error in catch-all route for {filename}: {str(e)}")
return Response(f"Error serving file {filename}", status=500)
"""-------------------------------------------------------------Main Views-----------------------------------------------------------------------------"""
@@ -3350,7 +3350,7 @@ def api_library_items():
}), 200
except Exception as e:
app.logger.error(f"Error fetching library items: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An error occurred while fetching library items'}), 500
@app.route('/api/library_scan_action', methods=['POST'])
@@ -3567,15 +3567,20 @@ def api_item_detail(item_id):
for rec in borrow_records:
if rec.get('Status') == 'active' and rec.get('User'):
try:
borrower_value = decrypt_text(rec.get('User'))
user_raw = rec.get('User')
if user_raw is not None:
borrower_value = decrypt_text(str(user_raw))
except Exception:
borrower_value = rec.get('User')
borrower_value = str(rec.get('User', ''))
break
if not borrower_value and item.get('User'):
try:
borrower_value = decrypt_text(item.get('User'))
item_user = item.get('User')
if item_user is not None:
borrower_value = decrypt_text(str(item_user))
except Exception:
borrower_value = item.get('User')
borrower_value = str(item.get('User', ''))
# Helper to format datetimes
def fmt_dt(dt):
@@ -3584,38 +3589,46 @@ def api_item_detail(item_id):
except Exception:
return str(dt) if dt else ''
# Build HTML for borrow records (if any)
borrows_html = ''
if borrow_records:
rows = []
for rec in borrow_records:
user_raw = rec.get('User') or ''
user_raw = rec.get('User')
try:
user = decrypt_text(user_raw) if user_raw else ''
user = decrypt_text(user_raw) if user_raw is not None else ''
except Exception:
user = user_raw
status = rec.get('Status', '')
start = fmt_dt(rec.get('Start'))
end = fmt_dt(rec.get('End'))
notes = html.escape(str(rec.get('Notes') or ''))
rows.append(f"<li><strong>{html.escape(user or '-')}</strong> — {html.escape(status)}{html.escape(start)}{html.escape(end)}{('' + notes) if notes else ''}</li>")
rows.append(
f"<li><strong>{html.escape(str(user or '-'))}</strong> — "
f"{html.escape(str(status))}"
f"{html.escape(str(start))}{html.escape(str(end))}"
f"{('' + notes) if notes else ''}</li>"
)
borrows_html = f"<h3>Ausleihhistorie</h3><ul>{''.join(rows)}</ul>"
# Basic detail HTML
detail_html = f"""
<h2>{html.escape(item.get('Name', 'Untitled'))}</h2>
<p><strong>Autor/Künstler:</strong> {html.escape(item.get('Autor', item.get('Author', '-')))}</p>
<p><strong>ISBN:</strong> {html.escape(item.get('ISBN', item.get('Code4', '-')))}</p>
<p><strong>Beschreibung:</strong> {html.escape(item.get('Beschreibung', '-'))}</p>
<p><strong>Status:</strong> {html.escape(status_label)}</p>
<h2>{html.escape(str(item.get('Name', 'Untitled')))}</h2>
<p><strong>ISBN:</strong> {html.escape(str(item.get('ISBN', item.get('Code4', '-'))))}</p>
<p><strong>Anzahl:</strong> {html.escape(str(item.get('SeriesCount', '-')))}</p>
<p><strong>Ort:</strong> {html.escape(str(item.get('Ort', '-')))}</p>
<p><strong>Typ:</strong> {html.escape(str(item.get('ItemType', '-')))}</p>
<p><strong>Kategorie:</strong> {html.escape(str(item.get('library_category', '-')))}</p>
<p><strong>Beschreibung:</strong> {html.escape(str(item.get('Beschreibung', '-')))}</p>
<p><strong>Status:</strong> {html.escape(str(status_label))}</p>
{f'<p><strong>Ausgeliehen von:</strong> {html.escape(str(borrower_value))}</p>' if borrower_value and status_label == 'Ausgeliehen' else ''}
{borrows_html}
"""
client.close()
return detail_html, 200
except Exception as e:
app.logger.error(f"Error fetching item detail: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An error occurred while fetching the item detail'}), 500
@app.route('/api/library_item/<item_id>/update', methods=['POST'])
@@ -4229,7 +4242,8 @@ def student_card_barcode_download():
download_name=f'schuelerausweise_all_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
)
except Exception as e:
flash(f'Fehler beim PDF-Download: {str(e)}', 'error')
app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {e}")
flash('Fehler beim PDF-Download', 'error')
return redirect(url_for('student_cards_admin'))
@@ -4401,7 +4415,8 @@ def student_card_single_barcode_download(card_id):
download_name=f'ausweis_{card["AusweisId"]}.pdf'
)
except Exception as e:
flash(f'Fehler beim PDF-Download: {str(e)}', 'error')
app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {e}")
flash('Fehler beim PDF-Download', 'error')
return redirect(url_for('student_cards_admin'))
@@ -4738,7 +4753,7 @@ def get_items():
'has_more': pagination_requested and ((offset + count) < total_count)
})
except Exception as e:
return jsonify({'items': [], 'error': str(e)}), 500
return jsonify({'items': []}), 500
finally:
if client:
client.close()
@@ -4755,7 +4770,8 @@ def get_item_json(id):
item['_id'] = str(item['_id'])
return jsonify(item)
except Exception as e:
return jsonify({'error': str(e)}), 500
app.logger.error(f"Error occurred while fetching item {id}: {e}")
return jsonify({'error': 'An error occurred while fetching the item'}), 500
@app.route('/get_bookings')
@@ -4829,7 +4845,7 @@ def get_bookings():
return jsonify({'ok': True, 'bookings': result})
except Exception as e:
return jsonify({'ok': False, 'error': str(e), 'bookings': []}), 500
return jsonify({'ok': False, 'bookings': []}), 500
finally:
if client:
client.close()
@@ -4919,7 +4935,7 @@ def get_user_appointments():
return jsonify({'ok': True, 'bookings': result})
except Exception as e:
return jsonify({'ok': False, 'error': str(e), 'bookings': []}), 500
return jsonify({'ok': False, 'bookings': []}), 500
finally:
if client:
client.close()
@@ -4963,7 +4979,8 @@ def api_booking_conflicts():
client.close()
return jsonify({'conflicts': result, 'count': len(result)})
except Exception as e:
return jsonify({'error': str(e), 'conflicts': []}), 500
app.logger.error(f"Error occurred while fetching booking conflicts: {e}")
return jsonify({'error': 'An error occurred while fetching booking conflicts', 'conflicts': []}), 500
"""Favorites management endpoints (persistent + session cache)."""
def _ensure_session_favs():
@@ -5078,7 +5095,8 @@ def debug_favorites():
try:
db_favs = us.get_favorites(username)
except Exception as e:
return jsonify({'ok': False, 'error': f'db_error: {e}', 'session': session_favs})
app.logger.error(f"Error fetching DB favorites: {e}")
return jsonify({'ok': False, 'error': 'Failed to fetch DB favorites', 'session': session_favs})
merged = sorted(set(session_favs) | set(db_favs))
return jsonify({'ok': True, 'user': username, 'session': session_favs, 'db': db_favs, 'merged': merged})
@@ -5139,7 +5157,8 @@ def upload_item():
individual_codes_raw = sanitize_form_value(request.form.get('individual_codes', ''))
item_count_raw = sanitize_form_value(request.form.get('item_count', '1'))
item_type_input = sanitize_form_value(request.form.get('item_type_input', ''))
library_category = sanitize_form_value(request.form.get('library_category', ''))
try:
item_count = int(item_count_raw) if item_count_raw else 1
except (TypeError, ValueError):
@@ -5151,12 +5170,12 @@ def upload_item():
if individual_codes_raw:
individual_codes = [c.strip() for c in str(individual_codes_raw).replace(',', '\n').splitlines() if c.strip()]
# Check if this is a duplication
is_duplicating = request.form.get('is_duplicating') == 'true'
# Get duplicate_images if duplicating
duplicate_images = request.form.getlist('duplicate_images') if is_duplicating else []
print(f"DEBUG: Duplicate images from form: {duplicate_images}, count: {len(duplicate_images)}")
# Make sure duplicate_images is always a list, even if there's only one
if is_duplicating and duplicate_images and not isinstance(duplicate_images, list):
@@ -5186,7 +5205,7 @@ def upload_item():
except json.JSONDecodeError as e:
app.logger.error(f"Error parsing mobile data: {str(e)}")
except Exception as e:
error_msg = f"Fehler beim Verarbeiten der Formulardaten: {str(e)}"
error_msg = f"Fehler beim Verarbeiten der Formulardaten"
app.logger.error(error_msg)
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
@@ -5297,7 +5316,7 @@ def upload_item():
# Validate every code in our master list against the database
for code in all_item_codes:
if not it.is_code_unique(code):
app.logger.info(f"DEBUG: Code '{code}' is not unique.")
# Special case: If they only uploaded ONE item, redirect them to that existing item
if item_count == 1:
existing_item = it._get_items_collection().find_one({"code_4": code})
@@ -5330,13 +5349,13 @@ def upload_item():
if not base_code:
return None
candidate = base_code if position == 1 else f"{base_code}-{position}"
candidate = base_code if position == 1 else f"{base_code}"
if it.is_code_unique(candidate):
return candidate
suffix = 1
while suffix <= 1000:
alternative = f"{candidate}-{suffix}"
alternative = f"{candidate}"
if it.is_code_unique(alternative):
return alternative
suffix += 1
@@ -5549,7 +5568,7 @@ def upload_item():
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Fallback PNG save also failed: {str(fallback_err)}")
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error type: {type(fallback_err).__name__}")
traceback.print_exc()
error_count += 1
continue
else:
@@ -5657,7 +5676,7 @@ def upload_item():
except Exception as webp_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Final WebP conversion failed: {str(webp_err)}")
traceback.print_exc()
error_count += 1
continue
@@ -5716,7 +5735,7 @@ def upload_item():
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Could not get PNG dimensions: {str(dim_err)}")
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error type: {type(dim_err).__name__}")
traceback.print_exc()
app.logger.info(f"{image_log_prefix} Starting optimization for {saved_filename} ({original_size/1024:.1f}KB, {original_dimensions})")
@@ -5766,7 +5785,6 @@ def upload_item():
app.logger.warning(f"{image_log_prefix} Optimization failed or returned no file")
except Exception as e:
app.logger.error(f"{image_log_prefix} Optimization failed: {str(e)}")
traceback.print_exc()
# No fallback thumbnail generation needed as we only use the main image
@@ -5778,7 +5796,6 @@ def upload_item():
except Exception as e:
app.logger.error(f"{image_log_prefix} Unexpected error: {str(e)}")
traceback.print_exc()
error_count += 1
# Continue with the next image
@@ -5915,7 +5932,7 @@ def upload_item():
app.logger.error(f"Error generating optimized versions for {new_filename}: {e}")
# If optimization fails, at least keep the original file
result = {'original': new_filename}
traceback.print_exc()
# If we didn't find the image, use a placeholder
else:
@@ -5951,7 +5968,7 @@ def upload_item():
app.logger.info(f"Processed image {i+1}/{original_count}: {new_filename}")
except Exception as e:
app.logger.error(f"Error processing image {i+1}/{original_count} ({dup_img}): {str(e)}")
traceback.print_exc()
error_count += 1
# Log placeholder usage
@@ -5989,9 +6006,6 @@ def upload_item():
else:
app.logger.warning(f"Book cover image not found: {book_cover_image}")
# Log image processing stats
app.logger.info(f"Upload stats: processed={processed_count}, errors={error_count}, skipped={skipped_count}, duplicates={len(duplicate_images) if duplicate_images else 0}")
# If location is not in the predefined list, add it
predefined_locations = it.get_predefined_locations()
if ort and ort not in predefined_locations:
@@ -6032,7 +6046,8 @@ def upload_item():
is_grouped_sub_item=(position > 1),
parent_item_id=parent_item_id,
isbn=item_isbn,
item_type=item_type
item_type=item_type,
library_category=library_category
)
if not item_id:
break
@@ -6185,7 +6200,7 @@ def duplicate_item():
except Exception as e:
print(f"Error in duplicate_item: {e}")
traceback.print_exc()
return jsonify({'success': False, 'message': 'Serverfehler beim Duplizieren'}), 500
@@ -6338,7 +6353,7 @@ def delete_item(id):
soft_deleted_borrows = int(delete_result.get('soft_deleted_borrows', 0))
archived_files = int((delete_result.get('archive') or {}).get('archived_files', 0))
except Exception as e:
app.logger.error(f"Error during soft-delete for item group {id}: {str(e)}")
app.logger.error(f"Error during soft-delete for item group {id}")
delete_success = False
archived_files = 0
finally:
@@ -6470,7 +6485,7 @@ def bulk_delete_items():
'group_item_ids': result.get('group_item_ids', []),
})
except Exception as e:
app.logger.error(f"Error during bulk delete: {str(e)}")
app.logger.error(f"Error during bulk delete for items {item_ids}: {e}")
return jsonify({'success': False, 'message': 'Fehler beim Sammellöschen.'}), 500
finally:
if client:
@@ -6586,17 +6601,26 @@ def edit_item(id):
if ort and ort not in predefined_locations:
it.add_predefined_location(ort)
# Update the item
result = it.update_item(
id, name, ort, beschreibung,
images, verfuegbar, filter1, filter2, filter3,
anschaffungs_jahr, anschaffungs_kosten, code_4, reservierbar,
id=id,
name=name,
ort=ort,
beschreibung=beschreibung,
images=images,
verfuegbar=verfuegbar,
filter1=filter1,
filter2=filter2,
filter3=filter3,
ansch_jahr=anschaffungs_jahr,
ansch_kost=anschaffungs_kosten,
code_4=code_4,
reservierbar=reservierbar,
isbn=item_isbn,
item_type=item_type
)
if result:
flash('Element erfolgreich aktualisiert', 'success')
flash('Element erfolgreich aktualisiert (und ggf. Gruppe synchronisiert)', 'success')
else:
flash('Fehler beim Aktualisieren des Elements', 'error')
@@ -7201,9 +7225,9 @@ def zurueckgeben(id):
)
except Exception as e:
print(f"Error in return process: {e}")
app.logger.error(f"Error in return process: {e}")
it.update_item_status(id, True)
flash(f'Element zurückgegeben, aber ein Fehler ist aufgetreten: {str(e)}', 'warning')
flash('Element zurückgegeben, aber ein Fehler ist aufgetreten', 'warning')
else:
flash('Sie sind nicht berechtigt, dieses Element zurückzugeben, oder es ist bereits verfügbar', 'error')
@@ -7300,7 +7324,7 @@ def get_planned_bookings(item_id):
client.close()
return jsonify({'ok': True, 'bookings': bookings})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
return jsonify({'ok': False}), 500
@app.route('/get_planned_bookings_public/<item_id>')
@@ -7326,7 +7350,7 @@ def get_planned_bookings_public(item_id):
client.close()
return jsonify({'ok': True, 'bookings': bookings})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
return jsonify({'ok': False}), 500
@app.route('/check_availability')
@@ -7407,7 +7431,7 @@ def check_availability():
client.close()
return jsonify({'ok': True, 'available': len(conflicts) == 0, 'conflicts': conflicts})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
return jsonify({'ok': False}), 500
# def create_qr_code(id):
@@ -7505,8 +7529,9 @@ def plan_booking():
if booking_type == 'single':
end_date = start_date
except ValueError as e:
return {"success": False, "error": f"Invalid date format: {e}"}, 400
app.logger.error(f"Invalid date format: {e}")
return {"success": False, "error": "Invalid date format"}, 400
# Check if item exists
item = it.get_item(item_id)
if not item:
@@ -7578,16 +7603,15 @@ def plan_booking():
}
else:
# All failed
return {"success": False, "errors": errors}, 400
return {"success": False}, 500
else:
# All succeeded
return {"success": True, "booking_ids": booking_ids}
except Exception as e:
import traceback
print(f"Error in plan_booking: {e}")
traceback.print_exc()
return {"success": False, "error": f"Serverfehler: {str(e)}"}, 500
app.logger.error(f"Error in plan_booking: {e}")
return {"success": False, "error": f"Fehler beim Planen der Buchung"}, 500
def process_day_bookings(item_id, booking_date, periods, notes):
"""
@@ -7682,7 +7706,7 @@ def add_booking():
return jsonify({'success': True, 'booking_id': str(booking_id)})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
return jsonify({'success': False})
@app.route('/cancel_booking/<id>', methods=['POST'])
def cancel_booking(id):
@@ -7733,9 +7757,7 @@ def terminplan():
return render_template('terminplan.html', school_periods=SCHOOL_PERIODS)
except Exception as e:
import traceback
print(f"Error rendering terminplan: {e}")
traceback.print_exc()
app.logger.error(f"Error rendering terminplan: {e}")
flash('Ein Fehler ist beim Anzeigen des Kalenders aufgetreten.', 'error')
return redirect(url_for('home'))
@@ -7921,15 +7943,17 @@ def delete_user():
client.close()
except Exception as e:
flash(f'Warnung: Ausleihungen/Reservierungen für {username} konnten nicht vollständig zurückgesetzt werden: {str(e)}', 'warning')
app.logger.error(f"Error resetting borrowings for user {encrypt_text(username)}: {e}")
flash(f'Warnung: Ausleihungen/Reservierungen für {username} konnten nicht vollständig zurückgesetzt werden', 'warning')
# Delete the user
try:
us.delete_user(username)
flash(f'Benutzer {username} erfolgreich gelöscht', 'success')
except Exception as e:
flash(f'Fehler beim Löschen des Benutzers: {str(e)}', 'error')
app.logger.error(f"Error deleting user {encrypt_text(username)}: {e}")
flash('Fehler beim Löschen des Benutzers', 'error')
return redirect(url_for('user_del'))
@@ -8027,7 +8051,7 @@ def admin_verify_audit_chain():
status_code = 200 if result.get('ok') else 409
return jsonify(result), status_code
except Exception as exc:
return jsonify({'ok': False, 'error': str(exc)}), 500
return jsonify({'ok': False, 'error': 'Internal server error'}), 500
finally:
if client:
client.close()
@@ -8046,7 +8070,7 @@ def admin_audit_dashboard():
al.ensure_audit_indexes(db)
verify_result = al.verify_audit_chain(db)
audit_rows = list(db['audit_log'].find(...).sort('chain_index', -1).limit(200))
audit_rows = list(db['audit_log'].find({}).sort('chain_index', -1).limit(200))
# DEC_START: Decrypt the sensitive fields for display
for row in audit_rows:
@@ -8098,7 +8122,7 @@ def admin_audit_export_pdf_official():
])
)
audit_rows = list(db['audit_log'].find(...).sort('chain_index', -1).limit(limit))
audit_rows = list(db['audit_log'].find({}).sort('chain_index', -1).limit(limit))
# DEC_START: Decrypt sensitive fields for the PDF report
for row in audit_rows:
@@ -8125,7 +8149,7 @@ def admin_audit_export_pdf_official():
except Exception as exc:
app.logger.error(f"PDF Official Report export error: {str(exc)}\n{traceback.format_exc()}")
return jsonify({'ok': False, 'error': str(exc)}), 500
return jsonify({'ok': False, 'error': 'Internal server error'}), 500
finally:
if client:
client.close()
@@ -8188,7 +8212,7 @@ def admin_image_cache_stats():
})
except Exception as e:
app.logger.error(f"Error getting cache stats: {str(e)}")
return jsonify({'ok': False, 'error': str(e)}), 500
return jsonify({'ok': False}), 500
@app.route('/admin/image_cache_cleanup', methods=['POST'])
@@ -8237,7 +8261,7 @@ def admin_image_cache_cleanup():
})
except Exception as e:
app.logger.error(f"Error during image cache cleanup: {str(e)}")
return jsonify({'ok': False, 'error': str(e)}), 500
return jsonify({'ok': False}), 500
"""-----------------------------------------------------------Borrowing Management Routes-------------------------------------------------------"""
@@ -8306,7 +8330,8 @@ def admin_reset_borrowing(borrow_id):
client.close()
except Exception as e:
flash(f'Fehler beim Zurücksetzen: {str(e)}', 'error')
app.logger.error(f"Error resetting borrowing status for {borrow_id}: {e}")
flash('Fehler beim Zurücksetzen', 'error')
return redirect(url_for('admin_borrowings'))
@@ -8920,10 +8945,11 @@ def admin_reset_user_password():
# Reset the password
try:
us.update_password(username, new_password)
flash(f'Passwort für {username} wurde erfolgreich zurückgesetzt auf: {new_password}', 'success')
flash(f'Passwort für {encrypt_text(username)} wurde erfolgreich zurückgesetzt auf: {new_password}', 'success')
except Exception as e:
flash(f'Fehler beim Zurücksetzen des Passworts: {str(e)}', 'error')
app.logger.error(f'Error resetting password for {encrypt_text(username)}: {e}')
flash('Fehler beim Zurücksetzen des Passworts', 'error')
return redirect(url_for('user_del'))
@@ -9423,7 +9449,8 @@ def search_word(word):
return jsonify({"success": True, "response": list(id_set)})
except Exception as e:
return jsonify({"success": False, "response": str(e)})
app.logger.error(f"Error searching for word: {e}")
return jsonify({"success": False})
def _fetch_from_google_books(clean_isbn):
"""Source 1: Google Books API (Free, No Key required for basic use)"""
@@ -9722,8 +9749,8 @@ def fetch_book_info(isbn):
return jsonify({"error": f"Kein Buch zu dieser ISBN gefunden: {clean_isbn}"}), 404
except Exception as e:
print(f"Error fetching book data: {e}")
return jsonify({"error": f"Failed to fetch book information: {str(e)}"}), 500
app.logger.error(f"Error fetching book data: {e}")
return jsonify({"error": f"Failed to fetch book information"}), 500
@app.route('/download_book_cover', methods=['POST'])
def download_book_cover():
@@ -9807,12 +9834,12 @@ def download_book_cover():
})
except requests.exceptions.RequestException as e:
print(f"Network error downloading book cover: {e}")
app.logger.error(f"Network error downloading book cover: {e}")
return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500
except Exception as e:
print(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": f"Failed to download image: {str(e)}"}), 500
return jsonify({"error": f"Failed to download image"}), 500
"""
@app.route('/proxy_image')
def proxy_image():
@@ -9871,8 +9898,8 @@ def proxy_image():
}
)
except Exception as e:
print(f"Error in proxy_image: {e}")
return jsonify({"error": f"Error fetching image: {str(e)}"}), 500
app.logger.error(f"Error in proxy_image: {e}")
return jsonify({"error": f"Error fetching image"}), 500
"""
@@ -10720,8 +10747,8 @@ def schedule_appointment():
if has_conflict:
return jsonify({'success': False, 'message': 'Termin kollidiert mit bestehender Buchung'}), 409
except Exception as e:
print(f"Error checking for booking conflicts: {e}")
return jsonify({'success': False, 'message': f'Fehler beim Prüfen der Verfügbarkeit: {str(e)}'}), 500
app.logger.error(f"Error checking for booking conflicts: {e}")
return jsonify({'success': False, 'message': f'Fehler beim Prüfen der Verfügbarkeit'}), 500
# Check if the appointment should already be active
now = datetime.datetime.now()
@@ -10792,8 +10819,8 @@ def schedule_appointment():
if not appointment_id:
return jsonify({'success': False, 'message': 'Termin konnte nicht erstellt werden'}), 500
except Exception as e:
print(f"Error creating booking: {e}")
return jsonify({'success': False, 'message': f'Fehler beim Erstellen des Termins: {str(e)}'}), 500
app.logger.error(f"Error creating booking: {e}")
return jsonify({'success': False, 'message': f'Fehler beim Erstellen des Termins'}), 500
# If we got this far, we have a valid appointment_id
try:
@@ -10827,14 +10854,14 @@ def schedule_appointment():
return jsonify({'success': False, 'message': 'Element konnte nicht mit Termininformationen aktualisiert werden'}), 500
except Exception as e:
print(f"Error updating item with appointment info: {e}")
return jsonify({'success': False, 'message': f'Fehler beim Aktualisieren des Elements: {str(e)}'}), 500
app.logger.error(f"Error updating item with appointment info: {e}")
return jsonify({'success': False, 'message': f'Fehler beim Aktualisieren des Elements'}), 500
except Exception as e:
print(f"Error creating appointment: {e}")
app.logger.error(f"Error creating appointment: {e}")
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': f'Serverfehler aufgetreten: {str(e)}'}), 500
return jsonify({'success': False, 'message': f'Serverfehler aufgetreten'}), 500
@app.route('/cancel_ausleihung/<id>', methods=['POST'])
def cancel_ausleihung_route(id):
@@ -10908,16 +10935,15 @@ def cancel_ausleihung_route(id):
next_appt = item_doc.get('NextAppointment', {})
if next_appt and str(next_appt.get('appointment_id')) == str(id):
cleared = it.clear_item_next_appointment(item_id)
print(f"Cleared NextAppointment for item {item_id}: {cleared}")
except Exception as clear_err:
print(f"Warning: could not clear NextAppointment for cancelled ausleihung {id}: {clear_err}")
app.logger.warning(f"Warning: could not clear NextAppointment for cancelled ausleihung {id}: {clear_err}")
else:
print(f"Failed to cancel ausleihung with ID: {id}")
app.logger.warning(f"Failed to cancel ausleihung with ID: {id}")
flash('Fehler beim Stornieren der Ausleihung', 'error')
except Exception as e:
print(f"Error canceling ausleihung: {e}")
flash(f'Fehler: {str(e)}', 'error')
app.logger.warning(f"Error canceling ausleihung: {e}")
flash(f'Fehler', 'error')
return redirect(url_for('my_borrowed_items'))
@@ -10948,22 +10974,21 @@ def reset_item(id):
if result['success']:
return jsonify({
'success': True,
'message': result['message'],
'details': result.get('details', {})
'message': 'Item reset successfully'
})
else:
return jsonify({
'success': False,
'message': result['message']
'message': 'Failed to reset item'
}), 400
except Exception as e:
print(f"Error in reset_item route: {e}")
import traceback
traceback.print_exc()
return jsonify({
'success': False,
'error': f'Serverfehler: {str(e)}'
'error': f'Serverfehler'
}), 500
# New image and video optimization functions
@@ -11109,7 +11134,6 @@ def create_image_thumbnail(image_path, thumbnail_path, size, debug_prefix=""):
return True
except Exception as e:
print(f"Error creating image thumbnail for {image_path}: {str(e)}")
return False
@@ -11150,11 +11174,11 @@ def create_video_thumbnail(video_path, thumbnail_path, size):
return success
else:
print(f"ffmpeg failed for {video_path}: {result.stderr}")
app.logger.error(f"ffmpeg failed for {video_path}: {result.stderr}")
return False
except Exception as e:
print(f"Error creating video thumbnail for {video_path}: {str(e)}")
app.logger.error(f"Error creating video thumbnail for {video_path}: {str(e)}")
return False
@@ -11381,7 +11405,7 @@ def generate_optimized_versions(filename, max_original_width=500, target_size_kb
except Exception as e:
app.logger.error(f"{log_prefix} Failed to process image: {str(e)}")
traceback.print_exc()
# Just copy the original file as is
if not is_webp_ext and os.path.exists(original_path) and not os.path.exists(converted_path):
try:
@@ -11410,7 +11434,7 @@ def generate_optimized_versions(filename, max_original_width=500, target_size_kb
except Exception as e:
app.logger.error(f"{log_prefix} Unhandled exception in optimization: {str(e)}")
traceback.print_exc()
# If anything went wrong but the original file exists, just use it
if os.path.exists(original_path):
@@ -11646,7 +11670,7 @@ def cleanup_old_optimized_images(max_age_days=30):
}
except Exception as e:
app.logger.error(f"Error during optimized image cleanup: {str(e)}")
return {'deleted': 0, 'freed_mb': 0, 'error': str(e)}
return {'deleted': 0, 'freed_mb': 0}
@app.route('/log_mobile_issue', methods=['POST'])
@@ -11691,7 +11715,7 @@ def log_mobile_issue():
return jsonify({'success': True})
except Exception as e:
app.logger.error(f"Error logging mobile issue: {str(e)}")
return jsonify({'success': False, 'error': str(e)})
return jsonify({'success': False})
def delete_item_images(filenames):
"""
@@ -11843,7 +11867,7 @@ def subscribe_to_push():
except Exception as e:
app.logger.error(f'Error subscribing to push: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
return jsonify({'success': False}), 500
@app.route('/api/push/unsubscribe', methods=['POST'])
@@ -11880,7 +11904,7 @@ def unsubscribe_from_push():
except Exception as e:
app.logger.error(f'Error unsubscribing from push: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
return jsonify({'success': False}), 500
@app.route('/api/push/subscriptions', methods=['GET'])
@@ -11914,7 +11938,7 @@ def get_push_subscriptions():
except Exception as e:
app.logger.error(f'Error getting push subscriptions: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
return jsonify({'success': False}), 500
@app.route('/api/push/vapid-key', methods=['GET'])
@@ -11939,7 +11963,7 @@ def get_vapid_key():
except Exception as e:
app.logger.error(f'Error getting VAPID key: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
return jsonify({'success': False}), 500
@app.route('/api/push/test', methods=['POST'])
@@ -11978,4 +12002,4 @@ def test_push_notification():
except Exception as e:
app.logger.error(f'Error sending test push: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
return jsonify({'success': False}), 500
+1 -1
View File
@@ -1235,5 +1235,5 @@ def reset_item_completely(item_id):
except Exception as e:
return {
'success': False,
'message': f'Fehler beim Zurücksetzen: {str(e)}'
'message': f'Fehler beim Zurücksetzen.'
}
+36 -40
View File
@@ -24,7 +24,7 @@ import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media')
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media', 'schulbuch')
def _non_library_query(extra_query=None):
@@ -49,7 +49,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True,
series_group_id=None, series_count=1, series_position=1,
is_grouped_sub_item=False, parent_item_id=None,
isbn=None, item_type='general'):
isbn=None, item_type='general', library_category=None):
"""
Add a new item to the inventory.
@@ -70,6 +70,9 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
series_position (int, optional): Position inside the batch (1-based)
is_grouped_sub_item (bool, optional): Whether this item is hidden as sub-item
parent_item_id (str, optional): Parent item id if this is a sub-item
isbn (str, optional): ISBN for books or media items
item_type (str, optional): Type of the item (e.g., 'general', 'book', 'cd')
library_category (str, optional): Library category for the item
Returns:
ObjectId: ID of the new item or None if failed
@@ -97,6 +100,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
'Anschaffungskosten': ansch_kost,
'Code_4': code_4,
'ISBN': isbn,
'library_category': library_category,
'ItemType': item_type,
'SeriesGroupId': series_group_id,
'SeriesCount': series_count,
@@ -194,69 +198,61 @@ def get_group_item_ids(id):
return []
def update_item(id, name, ort, beschreibung, images=None, verfuegbar=True,
filter=None, filter2=None, filter3=None, ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True,
isbn=None, item_type='general'):
"""
Update an existing inventory item.
Args:
id (str): ID of the item to update
name (str): Name of the item
ort (str): Location of the item
beschreibung (str): Description of the item
images (list, optional): List of image filenames for the item
verfuegbar (bool, optional): Availability status of the item
filter (str, optional): Primary filter/category for the item
filter2 (str, optional): Secondary filter/category for the item
filter3 (str, optional): Tertiary filter/category for the item
ansch_jahr (int, optional): Year of acquisition
ansch_kost (float, optional): Cost of acquisition
code_4 (str, optional): 4-digit identification code
reservierbar (bool, optional): Whether the item can be reserved in advance
Returns:
bool: True if successful, False otherwise
"""
def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3,
ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'):
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
items = db['items']
# Set default values for optional parameters
if images is None:
images = []
# 1. Altes Item laden, um SeriesGroupId zu bestimmen
old_item = items.find_one({'_id': ObjectId(id)})
if not old_item:
return False
series_group_id = old_item.get('SeriesGroupId')
update_data = {
# 2. Shared Data: Daten, die für ALLE in der Gruppe gleich sind
shared_update = {
'Name': name,
'Ort': ort,
'Beschreibung': beschreibung,
'Images': images,
'Verfuegbar': verfuegbar,
'Reservierbar': reservierbar,
'Filter': filter,
'Filter': filter1,
'Filter2': filter2,
'Filter3': filter3,
'Anschaffungsjahr': ansch_jahr,
'Anschaffungskosten': ansch_kost,
'Code_4': code_4,
'Reservierbar': reservierbar,
'ISBN': isbn,
'ItemType': item_type,
'Verfuegbar': verfuegbar, # Wir behalten den Status bei
'LastUpdated': datetime.datetime.now()
}
result = items.update_one(
{'_id': ObjectId(id)},
{'$set': update_data}
)
# 3. Spezifische Daten: Was NICHT synchronisiert wird
specific_update = shared_update.copy()
specific_update['Code_4'] = code_4
# 4. Das aktuelle Item updaten
items.update_one({'_id': ObjectId(id)}, {'$set': specific_update})
# 5. Alle anderen Gruppen-Mitglieder synchronisieren
if series_group_id:
items.update_many(
{
'SeriesGroupId': series_group_id,
'_id': {'$ne': ObjectId(id)}
},
{'$set': shared_update}
)
client.close()
return result.modified_count > 0
return True
except Exception as e:
print(f"Error updating item: {e}")
return False
def update_item_status(id, verfuegbar, user=None):
"""
Update the availability status of an inventory item.
+133 -6
View File
@@ -1057,7 +1057,7 @@
document.getElementById('detailModal').style.display = 'none';
}
// =========================================================================
// =========================================================================
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
// =========================================================================
function wireScannerUi() {
@@ -1192,7 +1192,6 @@
}
});
// Modal Control Functions (accessible globally from table buttons)
function openEditLibraryItem(itemId) {
const item = libraryItems.find(i => i._id === itemId);
@@ -1200,8 +1199,8 @@
alert('Fehler: Das Medium konnte nicht gefunden werden.');
return;
}
// Populate the form fields - Using correct German properties from MongoDB fields
// 1. Formular-Felder normal befüllen
document.getElementById('editLibraryItemId').value = item._id || '';
document.getElementById('editLibraryName').value = item.Name || '';
document.getElementById('editLibraryType').value = item.ItemType || 'book';
@@ -1209,19 +1208,131 @@
document.getElementById('editLibraryCode4').value = item.Code_4 || item.Code4 || '';
document.getElementById('editLibraryLocation').value = item.Ort || '';
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
// 2. Gruppen-Logik: Hole ALLE Items der Gruppe
const warningDiv = document.getElementById('editLibraryGroupWarning');
const groupCodeContainer = document.getElementById('editLibraryAllCodes');
// Prüfen ob Gruppenmitgliedschaft besteht
if (item.SeriesGroupId) {
// Alle Items mit gleicher ID finden und nach Position sortieren
const groupMembers = libraryItems
.filter(i => i.SeriesGroupId === item.SeriesGroupId)
.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0));
// Liste der Codes erstellen
let codesListHtml = `
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; margin-top:10px;">
`;
groupMembers.forEach(member => {
const isCurrent = member._id === itemId;
// Wenn Code_4 nicht existiert, zeige "Kein Code"
const codeVal = member.Code_4 || "---";
codesListHtml += `
<div style="text-align: center; padding: 5px; border-radius: 4px;
background: ${isCurrent ? '#0ea5e9' : '#f1f1f1'};
color: ${isCurrent ? '#fff' : '#333'};
border: 1px solid ${isCurrent ? '#0ea5e9' : '#ccc'};">
<span style="font-size: 10px; display:block;">Pos: ${member.SeriesPosition}</span>
<strong style="font-size: 14px;">${codeVal}</strong>
</div>
`;
});
codesListHtml += `</div>`;
document.getElementById('editLibraryGroupCount').textContent = groupMembers.length;
groupCodeContainer.innerHTML = codesListHtml;
warningDiv.style.display = 'block';
} else {
warningDiv.style.display = 'none';
}
document.getElementById('editLibraryModal').style.display = 'flex';
}
function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none';
}
// --- Sicherer Event-Listener für das Formular ---
document.addEventListener('DOMContentLoaded', function() {
const editForm = document.getElementById('editLibraryForm');
if (editForm) {
editForm.addEventListener('submit', async function(e) {
e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value;
const updateData = {
id: itemId,
name: document.getElementById('editLibraryName').value,
item_type: document.getElementById('editLibraryType').value,
isbn: document.getElementById('editLibraryIsbn').value,
code_4: document.getElementById('editLibraryCode4').value,
ort: document.getElementById('editLibraryLocation').value,
beschreibung: document.getElementById('editLibraryDescription').value
};
try {
const response = await fetch('/api/update-item', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
});
if (response.ok) {
const editedItem = libraryItems.find(i => i._id === itemId);
// Lokale Daten im Array synchronisieren
if (editedItem && editedItem.SeriesGroupId) {
libraryItems.forEach(item => {
if (item.SeriesGroupId === editedItem.SeriesGroupId) {
item.Name = updateData.name;
item.ItemType = updateData.item_type;
item.ISBN = updateData.isbn;
item.Ort = updateData.ort;
item.Beschreibung = updateData.beschreibung;
if (item._id === itemId) item.Code_4 = updateData.code_4;
}
});
} else if (editedItem) {
Object.assign(editedItem, updateData);
}
closeEditLibraryModal();
// Optional: renderLibraryTable();
} else {
const err = await response.json();
alert('Fehler beim Speichern: ' + (err.error || 'Unbekannter Fehler'));
}
} catch (error) {
console.error('Fetch error:', error);
alert('Netzwerkfehler.');
}
});
}
});
</script>
<div id="editLibraryModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 760px;">
<span class="close" onclick="closeEditLibraryModal()">&times;</span>
<h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3>
<!-- Bereich für Gruppen-Informationen -->
<div id="editLibraryGroupWarning" style="display:none; background-color: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #dee2e6;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<strong style="color: #0ea5e9;">Gruppen-Synchronisierung aktiv</strong>
<span style="font-size: 12px; color: #666;">Gesamt: <span id="editLibraryGroupCount"></span> Exemplare</span>
</div>
<p style="margin: 0 0 10px 0; font-size: 13px;">Codes in dieser Range:</p>
<div id="editLibraryAllCodes" style="display: flex; flex-wrap: wrap; gap: 5px;"></div>
</div>
<form id="editLibraryForm">
<input type="hidden" id="editLibraryItemId">
<div class="edit-grid">
@@ -1256,11 +1367,27 @@
<textarea id="editLibraryDescription" rows="4" required></textarea>
</div>
</div>
<div class="edit-actions">
<div class="edit-actions" style="margin-top:20px;">
<button type="submit" class="button" style="background:#0ea5e9;color:#fff;">Speichern</button>
<button type="button" class="button" onclick="closeEditLibraryModal()">Abbrechen</button>
</div>
</form>
</div>
</div>
<div id="editLibraryGroupWarning" style="display:none; background-color: #fff; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #0ea5e9;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<strong style="color: #0ea5e9;">Gruppen-Range (Total: <span id="editLibraryGroupCount"></span>)</strong>
</div>
<p style="margin: 5px 0; font-size: 12px; color: #555;">
Alle aufgeführten Codes gehören zu diesem Datensatz:
</p>
<!-- Hier werden die Codes per JS eingefügt -->
<div id="editLibraryAllCodes"></div>
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
</div>
</div>
{% endblock %}
+187
View File
@@ -880,6 +880,193 @@ PY
fi
;;
backup)
TENANT_ID="${2:-}"
if [ -z "$TENANT_ID" ]; then
echo "Error: Please provide a tenant_id."
exit 1
fi
OUTPUT_FILE="${3:-backup_${TENANT_ID}_$(date +%Y%m%d_%H%M%S).zip}"
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -z "$APP_CONTAINER" ]; then
echo "Error: Application container not running."
exit 1
fi
echo "Creating complete backup (.zip) for tenant '$TENANT_ID'..."
# We generate the zip temporarily inside the container
CONTAINER_TMP_ZIP="/tmp/backup_${TENANT_ID}_$(date +%s).zip"
docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" "$CONTAINER_TMP_ZIP" <<'PY'
import sys, os, zipfile
from bson import json_util
sys.path.insert(0, '/app')
sys.path.insert(0, '/app/Web')
from Web.modules.database import settings
from pymongo import MongoClient
tenant_id = sys.argv[1]
zip_path = sys.argv[2]
# CHANGE THIS if your app stores files in a different directory
UPLOADS_BASE = f"/app/uploads/{tenant_id}"
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
db_name = f"inventar_{sanitized}"
try:
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
db = client[db_name]
dump = {}
for coll_name in db.list_collection_names():
if coll_name.startswith('system.'):
continue
dump[coll_name] = list(db[coll_name].find())
db_json = json_util.dumps(dump)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
# 1. Write the database dump
zf.writestr('database.json', db_json)
# 2. Add uploaded photos/invoices if the directory exists
if os.path.isdir(UPLOADS_BASE):
for root, dirs, files in os.walk(UPLOADS_BASE):
for file in files:
file_path = os.path.join(root, file)
# Create a relative path for the zip internal structure
arcname = os.path.relpath(file_path, start=UPLOADS_BASE)
zf.write(file_path, arcname=f"uploads/{arcname}")
except Exception as e:
print(f"Error creating backup: {e}", file=sys.stderr)
sys.exit(1)
PY
if [ $? -eq 0 ]; then
# Copy the zip out of the container to the host
docker cp "$APP_CONTAINER:$CONTAINER_TMP_ZIP" "$OUTPUT_FILE"
# Cleanup inside the container
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
echo "Backup successfully created: $OUTPUT_FILE"
else
echo "Error: Backup failed."
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
exit 1
fi
;;
restore)
TENANT_ID="${2:-}"
INPUT_FILE="${3:-}"
if [ -z "$TENANT_ID" ] || [ -z "$INPUT_FILE" ]; then
echo "Error: Usage: $0 restore <tenant_id> <backup_file.zip>"
exit 1
fi
if [ ! -f "$INPUT_FILE" ]; then
echo "Error: Backup file '$INPUT_FILE' not found."
exit 1
fi
echo -n "WARNING: This will DROP the existing database and completely overwrite data AND files for tenant '$TENANT_ID'. Are you sure? (y/N) "
read confirm
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
echo "Restore canceled."
exit 0
fi
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -z "$APP_CONTAINER" ]; then
echo "Error: Application container not running."
exit 1
fi
echo "Uploading backup archive to container..."
CONTAINER_TMP_ZIP="/tmp/restore_${TENANT_ID}_$(date +%s).zip"
docker cp "$INPUT_FILE" "$APP_CONTAINER:$CONTAINER_TMP_ZIP"
echo "Restoring database and files for tenant '$TENANT_ID'..."
docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" "$CONTAINER_TMP_ZIP" <<'PY'
import sys, os, zipfile, shutil
from bson import json_util
sys.path.insert(0, '/app')
sys.path.insert(0, '/app/Web')
from Web.modules.database import settings
from pymongo import MongoClient
tenant_id = sys.argv[1]
zip_path = sys.argv[2]
# CHANGE THIS if your app stores files in a different directory
UPLOADS_BASE = f"/app/uploads/{tenant_id}"
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
db_name = f"inventar_{sanitized}"
try:
with zipfile.ZipFile(zip_path, 'r') as zf:
# 1. Restore Database
db_json = zf.read('database.json').decode('utf-8')
data = json_util.loads(db_json)
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
client.drop_database(db_name)
db = client[db_name]
restored_count = 0
for coll_name, docs in data.items():
if docs:
db[coll_name].insert_many(docs)
restored_count += len(docs)
# 2. Restore Files
# Wipe existing uploads to ensure an exact replica of the backup
if os.path.exists(UPLOADS_BASE):
shutil.rmtree(UPLOADS_BASE)
for item in zf.namelist():
# Extract only file items that were saved under 'uploads/'
if item.startswith('uploads/') and not item.endswith('/'):
rel_path = item[len('uploads/'):]
target_path = os.path.join(UPLOADS_BASE, rel_path)
os.makedirs(os.path.dirname(target_path), exist_ok=True)
with open(target_path, 'wb') as f:
f.write(zf.read(item))
print(f"Successfully restored {restored_count} database documents and tenant files.")
except Exception as e:
print(f"Error restoring backup: {e}", file=sys.stderr)
sys.exit(1)
PY
if [ $? -eq 0 ]; then
# Clean up tmp zip inside container
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
echo "Restore completed successfully."
echo "Clearing session cache..."
docker exec "$APP_CONTAINER" python3 -c "
import sys; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from Web.modules.database import settings; from pymongo import MongoClient
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
db = client[f'inventar_{"".join(c for c in sys.argv[1] if c.isalnum() or c == "_")}']
db.sessions.drop()
" "$TENANT_ID"
else
echo "Error: Restore failed."
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
exit 1
fi
;;
*)
echo "Unknown command: $COMMAND"
show_help