Compare commits

...

29 Commits

Author SHA1 Message Date
Aiirondev_dev 87027cf3c9 Changes to the damaged Items 2026-07-31 22:12:19 +02:00
Aiirondev_dev 46f79b002b Changes to the admin_damaged_items 2026-07-31 21:40:20 +02:00
Aiirondev_dev dcc8aae650 Implemenmtation of the new encryption into the my borrowed funktion 2026-07-31 21:28:34 +02:00
Aiirondev_dev c9fdf41e0b Implemenmtation of the new encryption into the my borrowed funktion 2026-07-31 21:21:43 +02:00
Aiirondev_dev 3e26c691b1 Addition of backwarts compatibility 2026-07-31 20:57:19 +02:00
Aiirondev_dev 6c3d62e84b Fix of a NoneType has no attribute 'get' Error 2026-07-31 20:50:58 +02:00
Aiirondev_dev 88f6c3c0f5 backwarts compaibility and integration of encryption in the manage-tenant.sh 2026-07-31 20:45:16 +02:00
Aiirondev_dev 08d8b78d91 changes to the encryption to the users and Items 2026-07-31 20:16:34 +02:00
Aiirondev_dev 258e287a39 changes to the encryption to the users and Items 2026-07-31 19:55:59 +02:00
Aiirondev_dev 9b12d9a3d0 changes to the encryption to the users and Items 2026-07-31 15:15:09 +02:00
Aiirondev_dev 237788af96 implementation of encryption for the username to avoid any recognission potetioal 2026-07-31 13:12:46 +02:00
Aiirondev_dev 7368d82fc4 changes to fully incorpereate the school info managment back into the working system 2026-07-30 23:55:46 +02:00
Aiirondev_dev 3e5e243ddf changes to the autorisation system for the page autorisation to have a continued line of page authentification and no discrepencies in the Software it self 2026-07-30 23:34:06 +02:00
Aiirondev_dev 8176cea8fe change of the HTML Signature 2026-07-30 16:52:34 +02:00
Aiirondev_dev b71f2e9089 changes 2026-07-30 01:09:58 +02:00
Aiirondev_dev 1331afa4da Fix of hexdigest 2026-07-29 13:40:17 +02:00
Aiirondev_dev 5a2d703bc7 Debug of Vapid Keys 2026-07-29 13:35:15 +02:00
Aiirondev_dev 8e6dd17243 Fix of the notification subscription 2026-07-29 11:57:34 +02:00
Aiirondev_dev 2124ca65b2 Fix of the notification subscription 2026-07-29 11:49:54 +02:00
Aiirondev_dev bc86dc4a0d Fix of the notification subscription 2026-07-29 11:43:18 +02:00
Aiirondev_dev a49ed98ed7 Fix of the notification subscription 2026-07-29 11:35:04 +02:00
Aiirondev_dev 4d9bb62907 Fix of the notification subscription 2026-07-29 11:15:57 +02:00
Aiirondev_dev 8251cd9bfd Fix of a dubled function 2026-07-29 00:22:35 +02:00
Aiirondev_dev 3ed3148b8a Fix of the notifikationssystem 2026-07-29 00:10:12 +02:00
Aiirondev_dev 27b265eaf5 Addition of a missing Funktion 2026-07-29 00:03:14 +02:00
Aiirondev_dev 3571bb6f6d Fix of the Notifications Funktions and the notifications system in generell in regarts to the Vapid key handeling 2026-07-28 23:46:15 +02:00
Aiirondev_dev b037434e89 fix correct VAPID key loading and public key assignment when PEM files exist 2026-07-28 23:21:35 +02:00
Aiirondev_dev ad14499df0 fix of the direction for _match_mail 2026-07-28 22:43:12 +02:00
Aiirondev_dev 6f50fb2263 Changes from old deployment system to new dynamic one 2026-07-28 22:09:40 +02:00
20 changed files with 1082 additions and 1694 deletions
+2 -47
View File
@@ -25,7 +25,6 @@ env:
jobs: jobs:
release-docker: release-docker:
# Hinweis: Falls dein lokaler Gitea-Runner ein anderes Label hat (z.B. 'linux' oder 'self-hosted'), passe dies hier an.
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -35,7 +34,6 @@ jobs:
- name: Set metadata - name: Set metadata
id: meta id: meta
env: env:
# Gitea stellt das GITHUB_TOKEN für Kompatibilität mit GitHub Actions automatisch zur Verfügung
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ gitea.repository }} REPO: ${{ gitea.repository }}
EVENT_NAME: ${{ gitea.event_name }} EVENT_NAME: ${{ gitea.event_name }}
@@ -46,7 +44,6 @@ jobs:
if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
TAG="$REF_NAME" TAG="$REF_NAME"
else else
# Gitea API Endpunkt nutzen, um das aktuellste Release abzufragen
latest_tag="v0.8.31" latest_tag="v0.8.31"
if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/json" "https://git.invario-software.eu/api/v1/repos/$REPO/releases/latest" 2>/dev/null); then if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/json" "https://git.invario-software.eu/api/v1/repos/$REPO/releases/latest" 2>/dev/null); then
tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
@@ -63,7 +60,6 @@ jobs:
major=0; minor=8; patch=31 major=0; minor=8; patch=31
fi fi
# Bump strategy: major / minor / patch
if [ "${BUMP_TYPE:-}" = "major" ]; then if [ "${BUMP_TYPE:-}" = "major" ]; then
major=$((major + 1)); minor=0; patch=0 major=$((major + 1)); minor=0; patch=0
elif [ "${BUMP_TYPE:-}" = "minor" ]; then elif [ "${BUMP_TYPE:-}" = "minor" ]; then
@@ -72,11 +68,9 @@ jobs:
patch=$((patch + 1)) patch=$((patch + 1))
fi fi
# Zusammenbau des Tags für den manuellen Run
TAG="v${major}.${minor}.${patch}" TAG="v${major}.${minor}.${patch}"
fi fi
# Validierung des erzeugten oder übergebenen Tags
if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-dev(\.[0-9]+)?)?$'; then if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-dev(\.[0-9]+)?)?$'; then
echo "Error: tag '$TAG' is not valid semver (vX.Y.Z)" echo "Error: tag '$TAG' is not valid semver (vX.Y.Z)"
exit 1 exit 1
@@ -95,13 +89,11 @@ jobs:
LATEST_MAJOR="0" LATEST_MAJOR="0"
fi fi
# If not explicitly bumping major, disallow changing major version
if [ "${BUMP_TYPE:-}" != "major" ] && [ "$TAG_MAJOR" != "$LATEST_MAJOR" ]; then if [ "${BUMP_TYPE:-}" != "major" ] && [ "$TAG_MAJOR" != "$LATEST_MAJOR" ]; then
echo "Error: major version must stay v$LATEST_MAJOR.x.x (got $TAG)" echo "Error: major version must stay v$LATEST_MAJOR.x.x (got $TAG)"
exit 1 exit 1
fi fi
# Ensure tag uniqueness: if tag exists append numeric suffix
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
i=1 i=1
base="$TAG" base="$TAG"
@@ -111,24 +103,18 @@ jobs:
TAG="${base}.${i}" TAG="${base}.${i}"
fi fi
# Docker Images verlangen Kleinbuchstaben. Repository-Namen daher umwandeln.
LOWER_REPO=$(echo "$REPO" | tr '[:upper:]' '[:lower:]') LOWER_REPO=$(echo "$REPO" | tr '[:upper:]' '[:lower:]')
IMAGE="git.invario-software.eu/${LOWER_REPO}:${TAG}" IMAGE="git.invario-software.eu/${LOWER_REPO}:${TAG}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT" echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
echo "lower_repo=$LOWER_REPO" >> "$GITHUB_OUTPUT" echo "lower_repo=$LOWER_REPO" >> "$GITHUB_OUTPUT"
- name: Ensure Docker CLI is available and up to date - name: Ensure Docker CLI is available and up to date
run: | run: |
install_docker=true install_docker=true
# Prüfen, ob Docker existiert und ob die Version ausreicht
if command -v docker >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1; then
# Extrahiere die Major-Version
DOCKER_MAJOR=$(docker --version | grep -oE '[0-9]+' | head -n1) DOCKER_MAJOR=$(docker --version | grep -oE '[0-9]+' | head -n1)
# API 1.44 erfordert mindestens Docker v25
if [ -n "$DOCKER_MAJOR" ] && [ "$DOCKER_MAJOR" -ge 25 ]; then if [ -n "$DOCKER_MAJOR" ] && [ "$DOCKER_MAJOR" -ge 25 ]; then
install_docker=false install_docker=false
fi fi
@@ -136,34 +122,19 @@ jobs:
if [ "$install_docker" = true ]; then if [ "$install_docker" = true ]; then
echo "Veraltete oder fehlende Docker-Installation erkannt. Lade statische Docker CLI herunter..." echo "Veraltete oder fehlende Docker-Installation erkannt. Lade statische Docker CLI herunter..."
DOCKER_VERSION="26.1.4" DOCKER_VERSION="26.1.4"
# Download der statischen Binaries via curl oder wget (umgeht apt-get komplett)
if command -v curl >/dev/null 2>&1; then if command -v curl >/dev/null 2>&1; then
curl -fsSLO "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" curl -fsSLO "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz"
else else
wget -q "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" wget -q "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz"
fi fi
tar -xzf docker-${DOCKER_VERSION}.tgz tar -xzf docker-${DOCKER_VERSION}.tgz
# Installation in lokalen Benutzer-Pfad, um sudo/root-Rechte-Probleme zu vermeiden
mkdir -p "$HOME/.local/bin" mkdir -p "$HOME/.local/bin"
cp docker/docker "$HOME/.local/bin/" cp docker/docker "$HOME/.local/bin/"
# Pfad für nachfolgende GitHub Actions Schritte verfügbar machen
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# Pfad für diesen spezifischen Shell-Run exportieren
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
rm -rf docker docker-${DOCKER_VERSION}.tgz rm -rf docker docker-${DOCKER_VERSION}.tgz
echo "Docker CLI wurde erfolgreich aktualisiert."
else
echo "Docker CLI ist bereits auf einem aktuellen Stand."
fi fi
docker --version docker --version
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -186,18 +157,6 @@ jobs:
${{ steps.meta.outputs.image }} ${{ steps.meta.outputs.image }}
git.invario-software.eu/${{ steps.meta.outputs.lower_repo }}:latest git.invario-software.eu/${{ steps.meta.outputs.lower_repo }}:latest
- name: Build local image tar for offline deploy
run: |
set -euo pipefail
IMG="${{ steps.meta.outputs.image }}"
TAG="${{ steps.meta.outputs.tag }}"
echo "Pulling freshly pushed image from registry: $IMG"
docker pull "$IMG"
echo "Saving image to offline tar archive..."
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
- name: Create release-only docker bundle - name: Create release-only docker bundle
run: | run: |
mkdir -p release-bundle mkdir -p release-bundle
@@ -275,21 +234,18 @@ jobs:
redis_data: redis_data:
EOF EOF
# Copy runtime scripts and config if present
for f in start.sh stop.sh restart.sh backup.sh restore.sh config.json update.sh; do for f in start.sh stop.sh restart.sh backup.sh restore.sh config.json update.sh; do
if [ -f "$f" ]; then if [ -f "$f" ]; then
cp "$f" "release-bundle/$(basename "$f")" cp "$f" "release-bundle/$(basename "$f")"
fi fi
done done
# Multitenant scripts & docs (optional)
for f in docker-compose-multitenant.yml manage-tenant.sh run-tenant-cmd.sh MULTITENANT_DEPLOYMENT.md MULTITENANT_PYTHON_API.md; do for f in docker-compose-multitenant.yml manage-tenant.sh run-tenant-cmd.sh MULTITENANT_DEPLOYMENT.md MULTITENANT_PYTHON_API.md; do
if [ -f "$f" ]; then if [ -f "$f" ]; then
cp "$f" "release-bundle/$(basename "$f")" cp "$f" "release-bundle/$(basename "$f")"
fi fi
done done
# Make any shipped scripts executable
find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle . tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
@@ -298,5 +254,4 @@ jobs:
with: with:
tag_name: ${{ steps.meta.outputs.tag }} tag_name: ${{ steps.meta.outputs.tag }}
files: | files: |
inventarsystem-docker-bundle.tar.gz inventarsystem-docker-bundle.tar.gz
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
+226 -213
View File
@@ -10248,74 +10248,58 @@ def get_period_times(booking_date, period_num):
"""---------------------------------------------------------Borrowing-----------------------------------------------------------------""" """---------------------------------------------------------Borrowing-----------------------------------------------------------------"""
@app.route('/my_borrowed_items') @app.route('/my_borrowed_items')
def my_borrowed_items(): def my_borrowed_items():
""" """
Zeigt alle vom aktuellen Benutzer ausgeliehenen und geplanten Objekte an. Zeigt alle vom aktuellen Benutzer ausgeliehenen und geplanten Objekte an.
Returns:
Response: Gerendertes Template mit den ausgeliehenen und geplanten Objekten des Benutzers
""" """
if 'username' not in session: if 'username' not in session:
flash('Bitte melden Sie sich an, um Ihre ausgeliehenen Objekte anzuzeigen', 'error') flash('Bitte melden Sie sich an, um Ihre ausgeliehenen Objekte anzuzeigen', 'error')
return redirect(url_for('login', next=request.path)) return redirect(url_for('login', next=request.path))
username = session['username'] username = session['username']
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
items_collection = db.items items_collection = db['items']
ausleihungen_collection = db.ausleihungen ausleihungen_collection = db['ausleihungen']
# Get current time for comparison all_ausleihungen = list(ausleihungen_collection.find({
current_time = datetime.datetime.now() 'Status': {'$in': ['active', 'planned']}
# Check if user is admin
user_is_admin = False
if 'is_admin' in session:
user_is_admin = session['is_admin']
# Get items currently borrowed by the user (where Verfuegbar=false and User=username)
borrowed_items = list(items_collection.find({'Verfuegbar': False, 'User': username}))
# Get active and planned ausleihungen for the user
active_ausleihungen = list(ausleihungen_collection.find({
'User': username,
'Status': 'active'
})) }))
planned_ausleihungen = list(ausleihungen_collection.find({
'User': username,
'Status': 'planned'
}))
# Process items
active_items = [] active_items = []
planned_items = [] planned_items = []
processed_item_ids = set() # Keep track of processed item IDs to avoid duplicates processed_item_ids = set()
# First, process items that are directly marked as borrowed by the user for appointment in all_ausleihungen:
for item in borrowed_items: raw_user = appointment.get('User', '')
# Convert ObjectId to string for template try:
item['_id'] = str(item['_id']) decrypted_user = decrypt_text(raw_user) if raw_user else ''
active_items.append(item) except Exception as e:
processed_item_ids.add(item['_id']) app.logger.error(f"Entschlüsselungsfehler: {e}")
decrypted_user = ''
# Process active appointments
for appointment in active_ausleihungen: if decrypted_user != username:
# Get the item ID from the appointment continue
item_id = appointment.get('Item') item_id = appointment.get('Item')
if not item_id:
if not item_id or str(item_id) in processed_item_ids: continue
continue # Skip if we already processed this item or no item ID
try:
# Get item details if isinstance(item_id, str):
item_obj = items_collection.find_one({'_id': ObjectId(item_id)}) query_id = ObjectId(item_id)
else:
query_id = item_id
item_obj = items_collection.find_one({'_id': query_id})
except Exception:
item_obj = None
if item_obj: if item_obj:
# Convert ObjectId to string for template
item_obj['_id'] = str(item_obj['_id']) item_obj['_id'] = str(item_obj['_id'])
# Add appointment data
item_obj['AppointmentData'] = { item_obj['AppointmentData'] = {
'id': str(appointment['_id']), 'id': str(appointment['_id']),
'start': appointment.get('Start'), 'start': appointment.get('Start'),
@@ -10324,70 +10308,76 @@ def my_borrowed_items():
'period': appointment.get('Period'), 'period': appointment.get('Period'),
'status': appointment.get('VerifiedStatus', appointment.get('Status')), 'status': appointment.get('VerifiedStatus', appointment.get('Status')),
} }
# Mark that this item is part of an active appointment status = appointment.get('Status')
item_obj['ActiveAppointment'] = True if status == 'active':
item_obj['ActiveAppointment'] = True
# Add to the list only if not already there if str(item_obj['_id']) not in processed_item_ids:
if str(item_obj['_id']) not in processed_item_ids: active_items.append(item_obj)
active_items.append(item_obj) processed_item_ids.add(str(item_obj['_id']))
processed_item_ids.add(str(item_obj['_id'])) elif status == 'planned':
planned_items.append(item_obj)
# Process planned appointments
for appointment in planned_ausleihungen: all_borrowed_items = list(items_collection.find({'Verfuegbar': False}))
item_id = appointment.get('Item') for item in all_borrowed_items:
raw_item_user = item.get('User', '')
if not item_id: try:
continue dec_item_user = decrypt_text(raw_item_user) if raw_item_user else ''
except Exception:
item_obj = items_collection.find_one({'_id': ObjectId(item_id)}) dec_item_user = ''
if item_obj: if dec_item_user == username and str(item['_id']) not in processed_item_ids:
item_obj['_id'] = str(item_obj['_id']) item['_id'] = str(item['_id'])
item['ActiveAppointment'] = True
# Add appointment data item['AppointmentData'] = {'status': 'active (no document)'}
item_obj['AppointmentData'] = { active_items.append(item)
'id': str(appointment['_id']), processed_item_ids.add(item['_id'])
'start': appointment.get('Start'),
'end': appointment.get('End'),
'notes': appointment.get('Notes'),
'period': appointment.get('Period'),
'status': appointment.get('Status'),
}
planned_items.append(item_obj)
client.close() client.close()
# DEBUG: Log what we're passing to the template # DEBUG Logging
app.logger.info(f"Passing {len(active_items)} active items and {len(planned_items)} planned items to template") app.logger.info(
if planned_items: f"Passing {len(active_items)} active items and {len(planned_items)} planned items to template for user {username}")
for i, item in enumerate(planned_items):
app.logger.info(f"Planned item {i+1}: {item['Name']}, Appointment ID: {item['AppointmentData']['id']}")
return render_template( return render_template(
'my_borrowed_items.html', 'my_borrowed_items.html',
items=active_items, items=active_items,
planned_items=planned_items, planned_items=planned_items
user_is_admin=user_is_admin
) )
@app.route('/api/push/vapid-key', methods=['GET'])
def get_vapid_key():
"""
Returns the VAPID public key for web push subscriptions
"""
from Web.push_notifications import _get_vapid_public
if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
try:
if not _get_vapid_public():
return jsonify({'success': False, 'error': 'VAPID public key not configured'}), 500
return jsonify({
'success': True,
'publicKey': _get_vapid_public()
})
except Exception as e:
app.logger.error(f'Error getting VAPID key: {e}')
return jsonify({'success': False}), 500
@app.route('/notifications') @app.route('/notifications')
def notifications_view(): def notifications_view():
"""Notification center for users and admins.""" """Notification center for users and admins."""
from Web.push_notifications import _get_vapid_public
if 'username' not in session: if 'username' not in session:
flash('Bitte melden Sie sich an, um Benachrichtigungen zu sehen.', 'error') flash('Bitte melden Sie sich an, um Benachrichtigungen zu sehen.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
username = session['username'] username = session['username']
is_admin_user = False
current_permissions = us.get_effective_permissions(session['username']) current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_manage_settings', False): is_admin_user = current_permissions['actions'].get('can_manage_settings', False)
is_admin_user = True
else:
is_admin_user = False
client = None client = None
try: try:
@@ -10399,15 +10389,17 @@ def notifications_view():
admin_notifications = [] admin_notifications = []
for notif in notifications: for notif in notifications:
is_read = username in (notif.get('ReadBy') or []) is_read = username in (notif.get('ReadBy') or [])
created_at_val = notif.get('CreatedAt')
row = { row = {
'id': str(notif.get('_id')), 'id': str(notif.get('_id')),
'title': notif.get('Title', 'Benachrichtigung'), 'title': notif.get('Title', 'Benachrichtigung'),
'message': notif.get('Message', ''), 'message': notif.get('Message', ''),
'severity': notif.get('Severity', 'info'), 'severity': notif.get('Severity', 'info'),
'type': notif.get('Type', ''), 'type': notif.get('Type', ''),
'created_at': notif.get('CreatedAt'), 'created_at': created_at_val if created_at_val else None,
'is_read': is_read, 'is_read': is_read,
'reference': notif.get('Reference', {}) or {}, 'reference': notif.get('Reference') or {},
} }
if notif.get('Audience') == 'admin': if notif.get('Audience') == 'admin':
admin_notifications.append(row) admin_notifications.append(row)
@@ -10421,6 +10413,7 @@ def notifications_view():
is_admin_user=is_admin_user, is_admin_user=is_admin_user,
library_module_enabled=cfg.MODULES.is_enabled('library'), library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'), student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
vapid_public_key=_get_vapid_public()
) )
except Exception as exc: except Exception as exc:
app.logger.error(f"Error loading notifications: {exc}") app.logger.error(f"Error loading notifications: {exc}")
@@ -10460,45 +10453,49 @@ def mark_notification_read(notification_id):
return redirect(url_for('notifications_view')) return redirect(url_for('notifications_view'))
@app.route('/notifications/mark_all_read', methods=['POST']) @app.route('/notifications/mark-all-read', methods=['POST'])
def mark_all_notifications_read(): def mark_all_notifications_read():
"""Mark all visible notifications as read for the current user."""
if 'username' not in session: if 'username' not in session:
flash('Bitte melden Sie sich an.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
username = session['username'] username = session['username']
is_admin_user = False
current_permissions = us.get_effective_permissions(session['username']) current_permissions = us.get_effective_permissions(username)
is_admin_user = current_permissions['actions'].get('can_manage_settings', False)
if not current_permissions['actions'].get('can_manage_settings', False):
is_admin_user = True
else:
is_admin_user = False
query = {
'$or': [
{'Audience': 'user', 'TargetUser': username},
]
}
if is_admin_user:
query['$or'].append({'Audience': 'admin'})
client = None client = None
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
result = db['notifications'].update_many(
# Filter-Logik: Welche Benachrichtigungen sollen als gelesen markiert werden?
# Wenn Sie 'Audience' nutzen (wie in Ihrer notifications_view Route):
query = {}
if is_admin_user:
# Ein Admin sieht sowohl an ihn gerichtete als auch allgemeine Admin-Meldungen
query['$or'] = [
{'TargetUser': username},
{'Audience': 'admin'}
]
else:
# Normale User sehen nur Meldungen, die an sie oder alle User gerichtet sind
query['$or'] = [
{'TargetUser': username},
{'Audience': 'user'}
]
# WICHTIG: Fügt den Benutzernamen per $addToSet in das ReadBy-Array ein.
# $addToSet verhindert, dass der Name doppelt eingetragen wird, falls er schon drinsteht.
db['notifications'].update_many(
query, query,
{ {'$addToSet': {'ReadBy': username}}
'$addToSet': {'ReadBy': username},
'$set': {'UpdatedAt': datetime.datetime.now()}
}
) )
if result.modified_count > 0:
_bump_notification_version(f'user:{username}') flash('Alle Benachrichtigungen wurden als gelesen markiert.', 'success')
except Exception as exc: except Exception as exc:
app.logger.warning(f"Could not mark all notifications as read for {encrypt_text(username)}: {exc}") app.logger.error(f"Error marking all notifications as read: {exc}")
flash('Fehler beim Aktualisieren der Benachrichtigungen.', 'error')
finally: finally:
if client: if client:
client.close() client.close()
@@ -10587,53 +10584,109 @@ def notifications_unread_status():
client.close() client.close()
@app.route('/admin/damaged_items') @app.route('/admin/damaged_items')
def admin_damaged_items(): def admin_damaged_items():
"""Admin-Übersicht aller aktiven und vergangenen Ausleihen.""" """Admin-Übersicht aller defekten, reparierten oder zerstörten Items."""
if 'username' not in session: if 'username' not in session:
flash('Administratorrechte erforderlich.', 'error') flash('Anmeldung erforderlich.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
from modules.inventarsystem.data_protection import decrypt_text from modules.inventarsystem.data_protection import decrypt_text
from bson.objectid import ObjectId
client = None client = None
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
ausleihungen_col = db['ausleihungen']
items_col = db['items'] items_col = db['items']
ausleihungen_col = db['ausleihungen']
ausleihungen = list(ausleihungen_col.find().sort('Start', -1)) # 1. Alle Items suchen, die einen Schaden haben, repariert oder zerstört sind
query = {
'$or': [
{'HasDamage': True},
{'DamageReports': {'$exists': True, '$not': {'$size': 0}}},
{'Condition': {'$in': ['destroyed', 'broken', 'damaged', 'repaired', 'fixed']}}
]
}
for record in ausleihungen: raw_items = list(items_col.find(query).sort('LastUpdated', -1))
raw_user = record.get('User', '') damaged_items = []
if raw_user:
record['User'] = decrypt_text(raw_user)
item_id = record.get('Item') for item in raw_items:
if item_id: # Dynamischen Status ermitteln (wie zuvor besprochen)
try: condition_value = str(item.get('Condition', '')).strip().lower()
item_doc = items_col.find_one({'_id': ObjectId(item_id)}) has_damage_flag = bool(item.get('HasDamage'))
if item_doc: has_reports = bool(item.get('DamageReports'))
if item_doc.get('User'):
item_doc['User'] = decrypt_text(item_doc['User']) if condition_value == 'destroyed':
status_text = 'Komplett zerstört'
record['ItemDetails'] = item_doc status_color = 'danger' # Rot
except Exception as e: elif condition_value in ['repaired', 'fixed'] or (has_reports and not has_damage_flag):
app.logger.warning(f"Konnte Item {item_id} für Ausleihe {record.get('_id')} nicht laden: {e}") status_text = 'Bereits repariert'
status_color = 'success' # Grün
else:
status_text = 'Aktuelles Problem'
status_color = 'warning' # Gelb
# Schadensberichte analysieren
damage_reports = item.get('DamageReports', [])
latest_report = damage_reports[-1] if damage_reports else {}
# Prüfen, ob es eine aktive oder geplante Ausleihe für dieses Item gibt
active_borrow = ausleihungen_col.find_one({
'Item': item.get('_id'), # Suche mit ObjectId
'Status': {'$in': ['active', 'planned']}
})
# Falls die ID in der Ausleihen-Collection als String hinterlegt ist (Fallback)
if not active_borrow:
active_borrow = ausleihungen_col.find_one({
'Item': str(item.get('_id')),
'Status': {'$in': ['active', 'planned']}
})
# Benutzernamen der Ausleihe entschlüsseln
if active_borrow and active_borrow.get('User'):
active_borrow['User'] = decrypt_text(active_borrow['User'])
# Direkt am Item hinterlegten Nutzer entschlüsseln
item_user = decrypt_text(item.get('User')) if item.get('User') else ''
# Datenstruktur exakt für dein Jinja2 Template aufbauen
damaged_items.append({
'id': str(item['_id']),
'name': item.get('Name', ''),
'code': item.get('Code_4', ''),
'item_type': item.get('ItemType', ''),
'author': item.get('Author', item.get('Autor', '')), # Deckt beide Schreibweisen ab
'isbn': item.get('ISBN', ''),
'borrow_user': item_user,
'damage_count': len(damage_reports),
'condition': item.get('Condition', '-'),
'available': item.get('Verfuegbar', False),
# Letzte Meldung
'latest_damage_description': latest_report.get('description', ''),
'latest_damage_by': latest_report.get('reported_by', ''),
'latest_damage_at': latest_report.get('reported_at', ''),
# Status & Ausleihe
'status_text': status_text,
'status_color': status_color,
'active_borrow': active_borrow
})
return render_template( return render_template(
'admin_damaged_items.html', 'admin_damaged_items.html',
ausleihungen=ausleihungen, damaged_items=damaged_items,
library_module_enabled=cfg.MODULES.is_enabled('library'), library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'), student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail') mail_module_enabled=cfg.MODULES.is_enabled('mail')
) )
except Exception as exc: except Exception as exc:
app.logger.error(f"Fehler beim Laden der Ausleihen-Verwaltung: {exc}") app.logger.error(f"Fehler beim Laden der defekten Items: {exc}")
flash('Fehler beim Laden der Ausleihen-Übersicht.', 'error') flash('Fehler beim Laden der Übersicht.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
finally: finally:
if client: if client:
@@ -12109,44 +12162,33 @@ def get_optimal_image_quality(img, target_size_kb=80):
def health_check(): def health_check():
return 'OK', 200 return 'OK', 200
@app.route('/api/push/subscribe', methods=['POST']) @app.route('/api/push/subscribe', methods=['POST'])
def subscribe_to_push(): def subscribe_to_push():
"""
Subscribe a user to push notifications
Expects JSON payload:
{
'subscription': {
'endpoint': '...',
'keys': {'p256dh': '...', 'auth': '...'}
}
}
"""
if 'username' not in session: if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401 return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json(silent=True) or {}
subscription_obj = data.get('subscription') if 'subscription' in data else data
if not subscription_obj or not isinstance(subscription_obj, dict):
app.logger.error("Invalid subscription payload received")
return jsonify({'success': False, 'error': 'Invalid payload'}), 400
username = session['username']
try: try:
data = request.get_json() or {} success = pn.save_push_subscription(username, subscription_obj)
subscription = data.get('subscription')
if not subscription or not subscription.get('endpoint'):
return jsonify({'success': False, 'error': 'Invalid subscription'}), 400
username = session['username']
success = pn.save_push_subscription(username, subscription)
if success: if success:
app.logger.info(f'Push subscription saved for {encrypt_text(username)}') return jsonify({'success': True})
return jsonify({
'success': True,
'message': 'Successfully subscribed to push notifications'
})
else: else:
return jsonify({'success': False, 'error': 'Failed to save subscription'}), 500 return jsonify({'success': False, 'error': 'Failed to save in DB'}), 400
except Exception as e: except Exception as e:
app.logger.error(f'Error subscribing to push: {e}') app.logger.error(f"Error in subscribe_to_push route: {e}")
return jsonify({'success': False}), 500 return jsonify({'success': False, 'error': 'Internal server error'}), 500
@app.route('/api/push/unsubscribe', methods=['POST']) @app.route('/api/push/unsubscribe', methods=['POST'])
@@ -12163,7 +12205,7 @@ def unsubscribe_from_push():
return jsonify({'success': False, 'error': 'Not authenticated'}), 401 return jsonify({'success': False, 'error': 'Not authenticated'}), 401
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
endpoint = data.get('endpoint') endpoint = data.get('endpoint')
if not endpoint: if not endpoint:
@@ -12219,32 +12261,6 @@ def get_push_subscriptions():
app.logger.error(f'Error getting push subscriptions: {e}') app.logger.error(f'Error getting push subscriptions: {e}')
return jsonify({'success': False}), 500 return jsonify({'success': False}), 500
@app.route('/api/push/vapid-key', methods=['GET'])
def get_vapid_key():
"""
Get the VAPID public key for push notifications
Used by the service worker to communicate with push service
"""
try:
vapid_key = pn.VAPID_PUBLIC_KEY
if not vapid_key:
app.logger.warning('VAPID_PUBLIC_KEY not configured')
return jsonify({
'success': False,
'error': 'Push notifications not configured on server'
}), 501
return jsonify({
'success': True,
'vapid_key': vapid_key
})
except Exception as e:
app.logger.error(f'Error getting VAPID key: {e}')
return jsonify({'success': False}), 500
@app.route('/api/push/test', methods=['POST']) @app.route('/api/push/test', methods=['POST'])
def test_push_notification(): def test_push_notification():
""" """
@@ -12253,11 +12269,8 @@ def test_push_notification():
if 'username' not in session: if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401 return jsonify({'success': False, 'error': 'Not authenticated'}), 401
if not us.is_admin(session['username']):
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
target_user = data.get('target_user', session['username']) target_user = data.get('target_user', session['username'])
sent = pn.send_push_notification( sent = pn.send_push_notification(
File diff suppressed because it is too large Load Diff
+205 -173
View File
@@ -19,11 +19,61 @@ Collection Structure:
- Status fields: Verfuegbar, User (if currently borrowed) - Status fields: Verfuegbar, User (if currently borrowed)
""" """
from bson.objectid import ObjectId from bson.objectid import ObjectId
from bson.errors import InvalidId
import datetime import datetime
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
def safe_decrypt_user(encrypted_user):
"""
Safely decrypt an encrypted username string.
Returns the original string if decryption fails or if input is empty/None.
"""
if not encrypted_user:
return encrypted_user
try:
return dp.decrypt_text(encrypted_user)
except Exception as e:
print(f"Error decrypting user data: {e}")
# Return fallback value or None to prevent downstream crashes
return "[Decryption Failed]"
def decrypt_item_user_data(item):
"""
Decrypts encrypted user fields within an inventory item document in-place.
Args:
item (dict): MongoDB document representing an item.
Returns:
dict: The item with decrypted user fields.
"""
if not item:
return item
# 1. Decrypt top-level 'User' field if present
if 'User' in item and item['User']:
item['User'] = safe_decrypt_user(item['User'])
# 2. Decrypt nested 'user' field in 'NextAppointment' if present
if 'NextAppointment' in item and isinstance(item['NextAppointment'], dict):
if 'user' in item['NextAppointment']:
item['NextAppointment']['user'] = safe_decrypt_user(item['NextAppointment']['user'])
return item
def _to_object_id(id_str):
"""Safely convert a string to ObjectId."""
try:
return ObjectId(id_str)
except (InvalidId, TypeError):
return None
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch') LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
@@ -52,7 +102,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
isbn=None, item_type='general', library_category=None, is_library=False): isbn=None, item_type='general', library_category=None, is_library=False):
""" """
Add a new item to the inventory. Add a new item to the inventory.
Args: Args:
name (str): Name of the item name (str): Name of the item
ort (str): Location of the item ort (str): Location of the item
@@ -73,7 +123,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
isbn (str, optional): ISBN for books or media items isbn (str, optional): ISBN for books or media items
item_type (str, optional): Type of the item (e.g., 'general', 'book', 'cd') item_type (str, optional): Type of the item (e.g., 'general', 'book', 'cd')
library_category (str, optional): Library category for the item library_category (str, optional): Library category for the item
Returns: Returns:
ObjectId: ID of the new item or None if failed ObjectId: ID of the new item or None if failed
""" """
@@ -102,7 +152,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
'ISBN': isbn, 'ISBN': isbn,
'library_category': library_category, 'library_category': library_category,
'ItemType': item_type, 'ItemType': item_type,
'is_library': is_library, 'is_library': is_library,
'SeriesGroupId': series_group_id, 'SeriesGroupId': series_group_id,
'SeriesCount': series_count, 'SeriesCount': series_count,
'SeriesPosition': series_position, 'SeriesPosition': series_position,
@@ -124,10 +174,10 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
def remove_item(id): def remove_item(id):
""" """
Soft-delete an item from the inventory. Soft-delete an item from the inventory.
Args: Args:
id (str): ID of the item to remove id (str): ID of the item to remove
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -199,21 +249,19 @@ def get_group_item_ids(id):
return [] return []
def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3, def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3,
ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'): ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'):
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# 1. Altes Item laden, um SeriesGroupId zu bestimmen
old_item = items.find_one({'_id': ObjectId(id)}) old_item = items.find_one({'_id': ObjectId(id)})
if not old_item: if not old_item:
return False return False
series_group_id = old_item.get('SeriesGroupId') series_group_id = old_item.get('SeriesGroupId')
# 2. Shared Data: Daten, die für ALLE in der Gruppe gleich sind
shared_update = { shared_update = {
'Name': name, 'Name': name,
'Ort': ort, 'Ort': ort,
@@ -227,18 +275,15 @@ def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter
'Reservierbar': reservierbar, 'Reservierbar': reservierbar,
'ISBN': isbn, 'ISBN': isbn,
'ItemType': item_type, 'ItemType': item_type,
'Verfuegbar': verfuegbar, # Wir behalten den Status bei 'Verfuegbar': verfuegbar,
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now()
} }
# 3. Spezifische Daten: Was NICHT synchronisiert wird
specific_update = shared_update.copy() specific_update = shared_update.copy()
specific_update['Code_4'] = code_4 specific_update['Code_4'] = code_4
# 4. Das aktuelle Item updaten
items.update_one({'_id': ObjectId(id)}, {'$set': specific_update}) items.update_one({'_id': ObjectId(id)}, {'$set': specific_update})
# 5. Alle anderen Gruppen-Mitglieder synchronisieren
if series_group_id: if series_group_id:
items.update_many( items.update_many(
{ {
@@ -257,12 +302,12 @@ def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter
def update_item_status(id, verfuegbar, user=None): def update_item_status(id, verfuegbar, user=None):
""" """
Update the availability status of an inventory item. Update the availability status of an inventory item.
Args: Args:
id (str): ID of the item to update id (str): ID of the item to update
verfuegbar (bool): New availability status verfuegbar (bool): New availability status
user (str, optional): Username of person who borrowed the item user (str, optional): Username of person who borrowed the item
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -279,7 +324,7 @@ def update_item_status(id, verfuegbar, user=None):
update_query = {'$set': update_data} update_query = {'$set': update_data}
if user is not None: if user is not None:
update_data['User'] = user update_data['User'] = dp.encrypt_text(user)
elif verfuegbar: elif verfuegbar:
# If item is being marked as available, clear the user field # If item is being marked as available, clear the user field
update_query['$unset'] = {'User': ""} update_query['$unset'] = {'User': ""}
@@ -299,11 +344,11 @@ def update_item_status(id, verfuegbar, user=None):
def update_item_exemplare_status(id, exemplare_status): def update_item_exemplare_status(id, exemplare_status):
""" """
Update the exemplar status of an inventory item. Update the exemplar status of an inventory item.
Args: Args:
id (str): ID of the item to update id (str): ID of the item to update
exemplare_status (list): List of status objects for each exemplar exemplare_status (list): List of status objects for each exemplar
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -332,32 +377,32 @@ def update_item_exemplare_status(id, exemplare_status):
def is_code_unique(code_4, exclude_id=None): def is_code_unique(code_4, exclude_id=None):
""" """
Check if a given code is unique (not used by any other item). Check if a given code is unique (not used by any other item).
Args: Args:
code_4 (str): The code to check code_4 (str): The code to check
exclude_id (str, optional): ID of item to exclude from the check (for edit operations) exclude_id (str, optional): ID of item to exclude from the check (for edit operations)
Returns: Returns:
bool: True if code is unique, False if already in use bool: True if code is unique, False if already in use
""" """
if not code_4 or code_4.strip() == "": if not code_4 or code_4.strip() == "":
# Empty codes are not considered unique # Empty codes are not considered unique
return False return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# Build query to find items with this code # Build query to find items with this code
query = {'Code_4': code_4, 'Deleted': {'$ne': True}} query = {'Code_4': code_4, 'Deleted': {'$ne': True}}
# If we're editing an item, exclude it from the uniqueness check # If we're editing an item, exclude it from the uniqueness check
if exclude_id: if exclude_id:
query['_id'] = {'$ne': ObjectId(exclude_id)} query['_id'] = {'$ne': ObjectId(exclude_id)}
# Check if any items with this code exist # Check if any items with this code exist
count = items.count_documents(query) count = items.count_documents(query)
client.close() client.close()
return count == 0 return count == 0
@@ -367,7 +412,7 @@ def is_code_unique(code_4, exclude_id=None):
def get_items(): def get_items():
""" """
Retrieve all inventory items. Retrieve all inventory items.
Returns: Returns:
list: List of all inventory item documents with string IDs list: List of all inventory item documents with string IDs
""" """
@@ -390,7 +435,7 @@ def get_items():
def get_available_items(): def get_available_items():
""" """
Retrieve all available inventory items. Retrieve all available inventory items.
Returns: Returns:
list: List of available inventory item documents with string IDs list: List of available inventory item documents with string IDs
""" """
@@ -413,7 +458,7 @@ def get_available_items():
def get_borrowed_items(): def get_borrowed_items():
""" """
Retrieve all currently borrowed inventory items. Retrieve all currently borrowed inventory items.
Returns: Returns:
list: List of borrowed inventory item documents with string IDs list: List of borrowed inventory item documents with string IDs
""" """
@@ -432,36 +477,36 @@ def get_borrowed_items():
print(f"Error retrieving borrowed items: {e}") print(f"Error retrieving borrowed items: {e}")
return [] return []
def get_item(id, decrypt=True):
"""
Retrieve an inventory item by ID, with optional decryption.
"""
item_id = _to_object_id(id)
if not item_id:
return None
def get_item(id):
"""
Retrieve a specific inventory item by its ID.
Args:
id (str): ID of the item to retrieve
Returns:
dict: The inventory item document or None if not found
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
item = items.find_one(_active_record_query({'_id': ObjectId(id)})) query = _active_record_query({'_id': item_id})
client.close() item = items.find_one(query)
if item:
item['_id'] = str(item['_id'])
if decrypt:
decrypt_item_user_data(item)
return item return item
except Exception as e: except Exception as e:
print(f"Error retrieving item: {e}") print(f"Error retrieving item {id}: {e}")
return None return None
def get_item_by_name(name): def get_item_by_name(name):
""" """
Retrieve a specific inventory item by its name. Retrieve a specific inventory item by its name.
Args: Args:
name (str): Name of the item to retrieve name (str): Name of the item to retrieve
Returns: Returns:
dict: The inventory item document or None if not found dict: The inventory item document or None if not found
""" """
@@ -480,10 +525,10 @@ def get_item_by_name(name):
def get_items_by_filter(filter_value): def get_items_by_filter(filter_value):
""" """
Retrieve inventory items matching a specific filter/category. Retrieve inventory items matching a specific filter/category.
Args: Args:
filter_value (str): Filter value to search for filter_value (str): Filter value to search for
Returns: Returns:
list: List of items matching the filter in primary, secondary, or tertiary category list: List of items matching the filter in primary, secondary, or tertiary category
""" """
@@ -491,7 +536,7 @@ def get_items_by_filter(filter_value):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# Use $or to find matches in any filter field # Use $or to find matches in any filter field
query = _active_record_query(_non_library_query({ query = _active_record_query(_non_library_query({
'$or': [ '$or': [
@@ -500,14 +545,14 @@ def get_items_by_filter(filter_value):
{'Filter3': filter_value} {'Filter3': filter_value}
] ]
})) }))
results = list(items.find(query)) results = list(items.find(query))
client.close() client.close()
# Convert ObjectId to string # Convert ObjectId to string
for item in results: for item in results:
item['_id'] = str(item['_id']) item['_id'] = str(item['_id'])
return results return results
except Exception as e: except Exception as e:
print(f"Error retrieving items by filter: {e}") print(f"Error retrieving items by filter: {e}")
@@ -517,7 +562,7 @@ def get_items_by_filter(filter_value):
def get_filters(): def get_filters():
""" """
Retrieve all unique filter/category values from the inventory. Retrieve all unique filter/category values from the inventory.
Returns: Returns:
list: Combined list of all primary, secondary and tertiary filter values list: Combined list of all primary, secondary and tertiary filter values
""" """
@@ -529,16 +574,16 @@ def get_filters():
filters = items.distinct('Filter', non_library) filters = items.distinct('Filter', non_library)
filters2 = items.distinct('Filter2', non_library) filters2 = items.distinct('Filter2', non_library)
filters3 = items.distinct('Filter3', non_library) filters3 = items.distinct('Filter3', non_library)
# Combine filters and remove None/empty values # Combine filters and remove None/empty values
all_filters = [f for f in filters + filters2 + filters3 if f] all_filters = [f for f in filters + filters2 + filters3 if f]
# Remove duplicates while preserving order # Remove duplicates while preserving order
unique_filters = [] unique_filters = []
for f in all_filters: for f in all_filters:
if f not in unique_filters: if f not in unique_filters:
unique_filters.append(f) unique_filters.append(f)
client.close() client.close()
return unique_filters return unique_filters
except Exception as e: except Exception as e:
@@ -549,7 +594,7 @@ def get_filters():
def get_primary_filters(): def get_primary_filters():
""" """
Retrieve all unique primary filter values. Retrieve all unique primary filter values.
Returns: Returns:
list: List of all primary filter values list: List of all primary filter values
""" """
@@ -559,7 +604,7 @@ def get_primary_filters():
items = db['items'] items = db['items']
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f] filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
client.close() client.close()
# Add predefined values # Add predefined values
predefined = get_predefined_filter_values(1) predefined = get_predefined_filter_values(1)
return sorted(list(set(filters + predefined))) return sorted(list(set(filters + predefined)))
@@ -571,7 +616,7 @@ def get_primary_filters():
def get_secondary_filters(): def get_secondary_filters():
""" """
Retrieve all unique secondary filter values. Retrieve all unique secondary filter values.
Returns: Returns:
list: List of all secondary filter values list: List of all secondary filter values
""" """
@@ -581,7 +626,7 @@ def get_secondary_filters():
items = db['items'] items = db['items']
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f] filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
client.close() client.close()
# Add predefined values # Add predefined values
predefined = get_predefined_filter_values(2) predefined = get_predefined_filter_values(2)
return sorted(list(set(filters + predefined))) return sorted(list(set(filters + predefined)))
@@ -593,7 +638,7 @@ def get_secondary_filters():
def get_tertiary_filters(): def get_tertiary_filters():
""" """
Retrieve all unique tertiary filter values. Retrieve all unique tertiary filter values.
Returns: Returns:
list: List of all tertiary filter values list: List of all tertiary filter values
""" """
@@ -603,7 +648,7 @@ def get_tertiary_filters():
items = db['items'] items = db['items']
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f] filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
client.close() client.close()
# Add predefined values # Add predefined values
predefined = get_predefined_filter_values(3) predefined = get_predefined_filter_values(3)
return sorted(list(set(filters + predefined))) return sorted(list(set(filters + predefined)))
@@ -615,10 +660,10 @@ def get_tertiary_filters():
def get_item_by_code_4(code_4): def get_item_by_code_4(code_4):
""" """
Retrieve inventory items matching a specific 4-digit code. Retrieve inventory items matching a specific 4-digit code.
Args: Args:
code_4 (str): 4-digit code to search for code_4 (str): 4-digit code to search for
Returns: Returns:
list: List of items matching the code list: List of items matching the code
""" """
@@ -627,11 +672,11 @@ def get_item_by_code_4(code_4):
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
results = list(items.find(_active_record_query(_non_library_query({"Code_4": code_4})))) results = list(items.find(_active_record_query(_non_library_query({"Code_4": code_4}))))
# Convert ObjectId to string # Convert ObjectId to string
for item in results: for item in results:
item['_id'] = str(item['_id']) item['_id'] = str(item['_id'])
client.close() client.close()
return results return results
except Exception as e: except Exception as e:
@@ -645,10 +690,10 @@ def unstuck_item(id):
""" """
Remove all borrowing records for a specific item to reset its status. Remove all borrowing records for a specific item to reset its status.
Used to fix problematic or stuck items. Used to fix problematic or stuck items.
Args: Args:
id (str): ID of the item to unstick id (str): ID of the item to unstick
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -664,7 +709,7 @@ def unstuck_item(id):
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now()
}} }}
) )
# Also reset the item status # Also reset the item status
items = db['items'] items = db['items']
items.update_one( items.update_one(
@@ -677,7 +722,7 @@ def unstuck_item(id):
'$unset': {'User': ""} '$unset': {'User': ""}
} }
) )
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
@@ -688,24 +733,24 @@ def unstuck_item(id):
def get_predefined_filter_values(filter_num): def get_predefined_filter_values(filter_num):
""" """
Get predefined values for a specific filter. Get predefined values for a specific filter.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
Returns: Returns:
list: List of predefined filter values list: List of predefined filter values
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
# Use a dedicated collection for filter presets # Use a dedicated collection for filter presets
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Find the document for the specified filter # Find the document for the specified filter
filter_doc = filter_presets.find_one({'filter_num': filter_num}) filter_doc = filter_presets.find_one({'filter_num': filter_num})
client.close() client.close()
if filter_doc and 'values' in filter_doc: if filter_doc and 'values' in filter_doc:
# Sort values alphabetically # Sort values alphabetically
return sorted(filter_doc['values']) return sorted(filter_doc['values'])
@@ -725,60 +770,60 @@ def get_predefined_filter_values(filter_num):
def add_predefined_filter_value(filter_num, value): def add_predefined_filter_value(filter_num, value):
""" """
Add a new predefined value to a filter. Add a new predefined value to a filter.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
value (str): Value to add value (str): Value to add
Returns: Returns:
bool: True if value was added, False if it already existed bool: True if value was added, False if it already existed
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Check if value already exists # Check if value already exists
filter_doc = filter_presets.find_one({ filter_doc = filter_presets.find_one({
'filter_num': filter_num, 'filter_num': filter_num,
'values': value 'values': value
}) })
if filter_doc: if filter_doc:
# Value already exists # Value already exists
client.close() client.close()
return False return False
# Add the value to the filter # Add the value to the filter
result = filter_presets.update_one( result = filter_presets.update_one(
{'filter_num': filter_num}, {'filter_num': filter_num},
{'$push': {'values': value}}, {'$push': {'values': value}},
upsert=True upsert=True
) )
client.close() client.close()
return result.modified_count > 0 or result.upserted_id is not None return result.modified_count > 0 or result.upserted_id is not None
def remove_predefined_filter_value(filter_num, value): def remove_predefined_filter_value(filter_num, value):
""" """
Remove a predefined value from a filter. Remove a predefined value from a filter.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
value (str): Value to remove value (str): Value to remove
Returns: Returns:
bool: True if value was removed, False otherwise bool: True if value was removed, False otherwise
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Remove the value from the filter # Remove the value from the filter
result = filter_presets.update_one( result = filter_presets.update_one(
{'filter_num': filter_num}, {'filter_num': filter_num},
{'$pull': {'values': value}} {'$pull': {'values': value}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
@@ -786,45 +831,45 @@ def remove_predefined_filter_value(filter_num, value):
def edit_predefined_filter_value(filter_num, old_value, new_value): def edit_predefined_filter_value(filter_num, old_value, new_value):
""" """
Edit a predefined value from a filter and update all matching items. Edit a predefined value from a filter and update all matching items.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
old_value (str): Value to replace old_value (str): Value to replace
new_value (str): New value new_value (str): New value
Returns: Returns:
bool: True if value was updated, False otherwise bool: True if value was updated, False otherwise
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Check if the new value already exists # Check if the new value already exists
existing = filter_presets.find_one({ existing = filter_presets.find_one({
'filter_num': filter_num, 'filter_num': filter_num,
'values': new_value 'values': new_value
}) })
if existing and old_value != new_value: if existing and old_value != new_value:
client.close() client.close()
return False return False
# Update the value in the filter # Update the value in the filter
result = filter_presets.update_one( result = filter_presets.update_one(
{'filter_num': filter_num, 'values': old_value}, {'filter_num': filter_num, 'values': old_value},
{'$set': {'values.$': new_value}} {'$set': {'values.$': new_value}}
) )
if result.modified_count > 0: if result.modified_count > 0:
items = db['items'] items = db['items']
filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}' filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}'
# Also update all items that use this filter # Also update all items that use this filter
items.update_many( items.update_many(
{filter_field: old_value}, {filter_field: old_value},
{'$set': {filter_field: new_value}} {'$set': {filter_field: new_value}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
@@ -862,22 +907,22 @@ def set_filter_name(filter_num, name):
def get_predefined_locations(): def get_predefined_locations():
""" """
Get list of all predefined locations/placement options. Get list of all predefined locations/placement options.
Returns: Returns:
list: List of predefined location strings list: List of predefined location strings
""" """
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
# Check if settings collection exists, create if not # Check if settings collection exists, create if not
if 'settings' not in db.list_collection_names(): if 'settings' not in db.list_collection_names():
db.create_collection('settings') db.create_collection('settings')
# Get settings document or create if it doesn't exist # Get settings document or create if it doesn't exist
settings_collection = db['settings'] settings_collection = db['settings']
location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'}) location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'})
if not location_settings: if not location_settings:
# Create default settings document if it doesn't exist # Create default settings document if it doesn't exist
settings_collection.insert_one({ settings_collection.insert_one({
@@ -885,12 +930,12 @@ def get_predefined_locations():
'locations': [] 'locations': []
}) })
return [] return []
# Return the predefined locations # Return the predefined locations
locations = location_settings.get('locations', []) locations = location_settings.get('locations', [])
client.close() client.close()
return sorted(locations) return sorted(locations)
except Exception as e: except Exception as e:
print(f"Error getting predefined locations: {str(e)}") print(f"Error getting predefined locations: {str(e)}")
return [] return []
@@ -899,28 +944,28 @@ def get_predefined_locations():
def add_predefined_location(location): def add_predefined_location(location):
""" """
Add a new predefined location. Add a new predefined location.
Args: Args:
location (str): Location to add location (str): Location to add
Returns: Returns:
bool: True if added successfully, False if already exists bool: True if added successfully, False if already exists
""" """
if not location or not isinstance(location, str): if not location or not isinstance(location, str):
return False return False
location = location.strip() location = location.strip()
if not location: if not location:
return False return False
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
settings_collection = db['settings'] settings_collection = db['settings']
# Check if settings document exists, create if not # Check if settings document exists, create if not
location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'}) location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'})
if not location_settings: if not location_settings:
# Create with the new location # Create with the new location
settings_collection.insert_one({ settings_collection.insert_one({
@@ -929,22 +974,22 @@ def add_predefined_location(location):
}) })
client.close() client.close()
return True return True
# Check if location already exists (case-insensitive) # Check if location already exists (case-insensitive)
current_locations = location_settings.get('locations', []) current_locations = location_settings.get('locations', [])
if any(loc.lower() == location.lower() for loc in current_locations): if any(loc.lower() == location.lower() for loc in current_locations):
client.close() client.close()
return False return False
# Add the new location # Add the new location
settings_collection.update_one( settings_collection.update_one(
{'setting_type': 'predefined_locations'}, {'setting_type': 'predefined_locations'},
{'$push': {'locations': location}} {'$push': {'locations': location}}
) )
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
print(f"Error adding predefined location: {str(e)}") print(f"Error adding predefined location: {str(e)}")
return False return False
@@ -953,29 +998,29 @@ def add_predefined_location(location):
def remove_predefined_location(location): def remove_predefined_location(location):
""" """
Remove a predefined location. Remove a predefined location.
Args: Args:
location (str): Location to remove location (str): Location to remove
Returns: Returns:
bool: True if removed successfully bool: True if removed successfully
""" """
if not location: if not location:
return False return False
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
settings_collection = db['settings'] settings_collection = db['settings']
result = settings_collection.update_one( result = settings_collection.update_one(
{'setting_type': 'predefined_locations'}, {'setting_type': 'predefined_locations'},
{'$pull': {'locations': location}} {'$pull': {'locations': location}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
print(f"Error removing predefined location: {str(e)}") print(f"Error removing predefined location: {str(e)}")
return False return False
@@ -984,17 +1029,12 @@ def remove_predefined_location(location):
def update_item_next_appointment(item_id, appointment_data): def update_item_next_appointment(item_id, appointment_data):
""" """
Update an item with information about its next scheduled appointment. Update an item with information about its next scheduled appointment.
Args: Args:
item_id (str): ID of the item to update item_id (str): ID of the item
appointment_data (dict): Appointment information containing: appointment_data (dict or None): Dictionary containing appointment details
- date: Date of the appointment (e.g., user, start_time, end_time) or None to clear it.
- start_period: Start period number
- end_period: End period number
- user: Username who scheduled the appointment
- notes: Optional notes
- appointment_id: ID of the appointment booking
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -1002,35 +1042,33 @@ def update_item_next_appointment(item_id, appointment_data):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# Format the appointment data for storage # If clearing the appointment
# Ensure date is a datetime object for MongoDB storage if appointment_data is None:
appointment_date = appointment_data['date'] update_query = {
if isinstance(appointment_date, datetime.date) and not isinstance(appointment_date, datetime.datetime): '$unset': {'NextAppointment': ""},
# Convert date to datetime for MongoDB compatibility '$set': {'LastUpdated': datetime.datetime.now()}
appointment_date = datetime.datetime.combine(appointment_date, datetime.time()) }
else:
next_appointment = { # Create a copy so we don't mutate the original dictionary passed in
'date': appointment_date, data_to_save = appointment_data.copy()
'end_date': appointment_data.get('end_date', appointment_date),
'start_period': appointment_data['start_period'], # Encrypt the user field if it exists to match the decryption logic at the top
'end_period': appointment_data['end_period'], if 'user' in data_to_save and data_to_save['user']:
'user': appointment_data['user'], data_to_save['user'] = dp.encrypt_text(data_to_save['user'])
'notes': appointment_data.get('notes', ''),
'appointment_id': appointment_data['appointment_id'], update_query = {
'scheduled_at': datetime.datetime.now() '$set': {
} 'NextAppointment': data_to_save,
'LastUpdated': datetime.datetime.now()
update_data = { }
'NextAppointment': next_appointment, }
'LastUpdated': datetime.datetime.now()
}
result = items.update_one( result = items.update_one(
{'_id': ObjectId(item_id)}, {'_id': ObjectId(item_id)},
{'$set': update_data} update_query
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
@@ -1041,10 +1079,10 @@ def update_item_next_appointment(item_id, appointment_data):
def clear_item_next_appointment(item_id): def clear_item_next_appointment(item_id):
""" """
Clear the next appointment information from an item. Clear the next appointment information from an item.
Args: Args:
item_id (str): ID of the item to update item_id (str): ID of the item to update
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -1052,12 +1090,12 @@ def clear_item_next_appointment(item_id):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
result = items.update_one( result = items.update_one(
{'_id': ObjectId(item_id)}, {'_id': ObjectId(item_id)},
{'$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now()}} {'$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now()}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
@@ -1068,7 +1106,7 @@ def clear_item_next_appointment(item_id):
def get_items_with_appointments(): def get_items_with_appointments():
""" """
Retrieve all items that have scheduled appointments. Retrieve all items that have scheduled appointments.
Returns: Returns:
list: List of items with NextAppointment field list: List of items with NextAppointment field
""" """
@@ -1076,7 +1114,7 @@ def get_items_with_appointments():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
items_return = items.find({'NextAppointment': {'$exists': True}, 'Deleted': {'$ne': True}}) items_return = items.find({'NextAppointment': {'$exists': True}, 'Deleted': {'$ne': True}})
items_list = [] items_list = []
for item in items_return: for item in items_return:
@@ -1088,31 +1126,25 @@ def get_items_with_appointments():
print(f"Error retrieving items with appointments: {e}") print(f"Error retrieving items with appointments: {e}")
return [] return []
def get_current_status(item_id): def get_current_status(item_id, decrypt=True):
""" """
Retrieve the current status of an item, including availability and user. Retrieve the current status of an item, decrypting the user field if present.
Args:
item_id (str): ID of the item to check
Returns:
dict: Current status of the item or None if not found
""" """
oid = _to_object_id(item_id)
if not oid:
return None
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
item = items.find_one({'_id': oid}, {'Verfuegbar': 1, 'User': 1})
item = items.find_one({'_id': ObjectId(item_id)}, {'Verfuegbar': 1, 'User': 1})
if item: if item:
# Convert ObjectId to string for consistency
item['_id'] = str(item['_id']) item['_id'] = str(item['_id'])
client.close() if decrypt:
decrypt_item_user_data(item)
return item return item
else: return None
client.close()
return None
except Exception as e: except Exception as e:
print(f"Error retrieving current status: {e}") print(f"Error retrieving current status for item {item_id}: {e}")
return None return None
+1 -1
View File
@@ -300,7 +300,7 @@ def _match_student_cards(path):
def _match_mail(path): def _match_mail(path):
if not path: return False if not path: return False
return path.startswith(('/')) return path.startswith(('/configure'))
# Register core modules into the pipeline # Register core modules into the pipeline
MODULES.register('inventory', INVENTORY_MODULE_ENABLED, _match_inventory) MODULES.register('inventory', INVENTORY_MODULE_ENABLED, _match_inventory)
+135 -252
View File
@@ -20,6 +20,7 @@ import string
from bson.objectid import ObjectId from bson.objectid import ObjectId
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
import hmac import hmac
import os import os
@@ -109,13 +110,6 @@ def build_username_from_name(first_name, last_name=''):
""" """
Build a deterministic username abbreviation from first and last name. Build a deterministic username abbreviation from first and last name.
Uses 3 letters from each name and stores it lowercase. Uses 3 letters from each name and stores it lowercase.
Args:
first_name (str): First name
last_name (str): Last name (optional)
Returns:
str: Generated username
""" """
alias = build_name_synonym(first_name, last_name) alias = build_name_synonym(first_name, last_name)
return alias.lower() return alias.lower()
@@ -324,7 +318,6 @@ def get_effective_permissions(username):
return build_default_permission_payload('full_access') return build_default_permission_payload('full_access')
preset_key = user.get('PermissionPreset') preset_key = user.get('PermissionPreset')
print(preset_key)
payload = build_default_permission_payload(preset_key) payload = build_default_permission_payload(preset_key)
payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions']) payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions'])
payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages']) payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages'])
@@ -352,10 +345,10 @@ def update_user_permissions(username, preset_key, action_permissions=None, page_
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one({'Username': username}, {'$set': update_data}) result = users.update_one({'Username': dp.encrypt_text(username)}, {'$set': update_data})
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one({'username': username}, {'$set': update_data}) result = users.update_one({'username': dp.encrypt_text(username)}, {'$set': update_data})
client.close() client.close()
return result.matched_count > 0 return result.matched_count > 0
@@ -367,7 +360,7 @@ def get_favorites(username):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username}) or users.find_one({'username': username}) user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
client.close() client.close()
if not user: if not user:
return [] return []
@@ -382,7 +375,7 @@ def add_favorite(username, item_id):
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
users.update_one( users.update_one(
{'$or': [{'Username': username}, {'username': username}]}, {'$or': [{'Username': dp.encrypt_text(username)}, {'username': dp.encrypt_text(username)}]},
{'$addToSet': {'favorites': ObjectId(item_id)}} {'$addToSet': {'favorites': ObjectId(item_id)}}
) )
client.close() client.close()
@@ -397,7 +390,7 @@ def remove_favorite(username, item_id):
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
users.update_one( users.update_one(
{'$or': [{'Username': username}, {'username': username}]}, {'$or': [{'Username': dp.encrypt_text(username)}, {'username': dp.encrypt_text(username)}]},
{'$pull': {'favorites': ObjectId(item_id)}} {'$pull': {'favorites': ObjectId(item_id)}}
) )
client.close() client.close()
@@ -406,16 +399,9 @@ def remove_favorite(username, item_id):
return False return False
def check_password_strength(password): def check_password_strength(password):
""" """
Check if a password meets minimum security requirements. Check if a password meets minimum security requirements.
Args:
password (str): Password to check
Returns:
bool: True if password is strong enough, False otherwise
""" """
if password is None: if password is None:
return False return False
@@ -435,19 +421,15 @@ def check_password_strength(password):
def hashing(password, salt=None): def hashing(password, salt=None):
""" """
Hasht ein Passwort mit scrypt. Hasht ein Passwort mit scrypt.
- Wenn kein Salt übergeben wird, wird ein sicherer, zufälliger Salt generiert (für neue Passwörter).
- Format für neue Hashes: v1$<salt_hex>$<hash_hex>
""" """
password_bytes = password.encode('utf-8') # Explizit UTF-8 für Plattformunabhängigkeit password_bytes = password.encode('utf-8')
if salt is None: if salt is None:
# Neuer Benutzer / Passwortänderung -> Dynamischer Salt
random_salt = os.urandom(16) random_salt = os.urandom(16)
hashed = hashlib.scrypt(password_bytes, salt=random_salt, n=16384, r=8, p=1) hashed = hashlib.scrypt(password_bytes, salt=random_salt, n=16384, r=8, p=1)
return f"v1${random_salt.hex()}${hashed.hex()}" return f"v1${random_salt.hex()}${hashed.hex()}"
else: else:
# Bestehender Benutzer (wird zur Verifizierung aufgerufen)
hashed = hashlib.scrypt(password_bytes, salt=salt, n=16384, r=8, p=1) hashed = hashlib.scrypt(password_bytes, salt=salt, n=16384, r=8, p=1)
return hashed.hex() return hashed.hex()
@@ -455,25 +437,20 @@ def hashing(password, salt=None):
def verify_password(provided_password, stored_password_string): def verify_password(provided_password, stored_password_string):
""" """
Verifiziert ein Passwort gegen einen gespeicherten Hash-String. Verifiziert ein Passwort gegen einen gespeicherten Hash-String.
Unterstützt das alte Format (statischer Salt) und das neue Format (v1$...).
""" """
if not stored_password_string: if not stored_password_string:
return False return False
# Überprüfung für das neue, sichere Format
if stored_password_string.startswith("v1$"): if stored_password_string.startswith("v1$"):
try: try:
_, salt_hex, hash_hex = stored_password_string.split("$") _, salt_hex, hash_hex = stored_password_string.split("$")
salt_bytes = bytes.fromhex(salt_hex) salt_bytes = bytes.fromhex(salt_hex)
# Berechne den Hash des eingegebenen Passworts mit dem extrahierten Salt
calculated_hash = hashing(provided_password, salt=salt_bytes) calculated_hash = hashing(provided_password, salt=salt_bytes)
# Timing-Attack-sicherer Vergleich
return hmac.compare_digest(calculated_hash, hash_hex) return hmac.compare_digest(calculated_hash, hash_hex)
except (ValueError, TypeError): except (ValueError, TypeError):
logger.error("Ungültiges Hash-Format in der Datenbank entdeckt.") logger.error("Ungültiges Hash-Format in der Datenbank entdeckt.")
return False return False
else: else:
# Abwärtskompatibilität: Altes Format mit statischem Salt b'some_salt'
old_static_salt = b'some_salt' old_static_salt = b'some_salt'
calculated_hash = hashing(provided_password, salt=old_static_salt) calculated_hash = hashing(provided_password, salt=old_static_salt)
return hmac.compare_digest(calculated_hash, stored_password_string) return hmac.compare_digest(calculated_hash, stored_password_string)
@@ -494,22 +471,26 @@ def check_nm_pwd(username, password):
try: try:
db = client[db_name] db = client[db_name]
users = db['users'] users = db['users']
query = {'$or': [{'Username': username}, {'username': username}]} query = {'$or': [{'Username': dp.encrypt_text(username)}, {'username': dp.encrypt_text(username)}]}
user_record = users.find_one(query) user_record = users.find_one(query)
if user_record is None: if user_record is None:
logger.warning("Kein Benutzer für %r in DB %r gefunden.", username, db_name) query = {'$or': [{'Username': username}, {'username': username}]}
return None user_record_fallback = users.find_one(query)
if user_record_fallback is None:
logger.warning("Kein Benutzer für %r in DB %r gefunden.", dp.encrypt_text(username), db_name)
return None
else:
user_record = user_record_fallback
stored_password = user_record.get('Password') or user_record.get('password') stored_password = user_record.get('Password') or user_record.get('password')
if not verify_password(password, stored_password): if not verify_password(password, stored_password):
logger.warning("Falsches Passwort für Benutzer %r in DB %r.", username, db_name) logger.warning("Falsches Passwort für Benutzer %r in DB %r.", dp.encrypt_text(username), db_name)
return None return None
# Automatische Migration alter Hashes auf das neue Format if stored_password and not stored_password.startswith("v1$"):
if not stored_password.startswith("v1$"):
users.update_one({'_id': user_record['_id']}, {'$set': {'Password': hashing(password)}}) users.update_one({'_id': user_record['_id']}, {'$set': {'Password': hashing(password)}})
return user_record return user_record
@@ -531,40 +512,35 @@ def add_user(
): ):
""" """
Add a new user to the database. Add a new user to the database.
Args:
username (str): Username for the new user
password (str): Password for the new user
Returns:
bool: True if user was added successfully, False if password was too weak
""" """
if not check_password_strength(password):
return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try: try:
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
if not check_password_strength(password):
return False
permission_defaults = build_default_permission_payload(permission_preset) permission_defaults = build_default_permission_payload(permission_preset)
if isinstance(action_permissions, dict): if isinstance(action_permissions, dict):
for key, value in action_permissions.items(): for key, value in action_permissions.items():
permission_defaults['actions'][str(key)] = bool(value) permission_defaults['actions'][str(key)] = bool(value)
if isinstance(page_permissions, dict): if isinstance(page_permissions, dict):
for key, value in page_permissions.items(): for key, value in page_permissions.items():
permission_defaults['pages'][str(key)] = bool(value) permission_defaults['pages'][str(key)] = bool(value)
if permission_preset == "full_access": safe_name = name.strip() if name else ''
can_admin_preset_based = True safe_last_name = last_name.strip() if last_name else ''
else:
can_admin_preset_based = False
user_doc = { user_doc = {
'Username': username, 'Username': dp.encrypt_text(username),
'Password': hashing(password), 'Password': hashing(password),
'Admin': can_admin_preset_based, 'Admin': (permission_preset == "full_access"),
'active_ausleihung': None, 'active_ausleihung': None,
'name': name.strip() if name else '', 'name': dp.encrypt_text(safe_name) if safe_name else '',
'last_name': last_name.strip() if last_name else '', 'last_name': dp.encrypt_text(safe_last_name) if safe_last_name else '',
'IsStudent': bool(is_student), 'IsStudent': bool(is_student),
'PermissionPreset': permission_defaults['preset'], 'PermissionPreset': permission_defaults['preset'],
'ActionPermissions': permission_defaults['actions'], 'ActionPermissions': permission_defaults['actions'],
@@ -601,7 +577,7 @@ def student_card_exists(student_card_id):
def get_user_by_student_card(student_card_id): def get_user_by_student_card(student_card_id):
"""Return user by student card id or None.""" """Return user dict by student card id or None."""
normalized = normalize_student_card_id(student_card_id) normalized = normalize_student_card_id(student_card_id)
if not normalized: if not normalized:
return None return None
@@ -610,65 +586,44 @@ def get_user_by_student_card(student_card_id):
users = db['student_cards'] users = db['student_cards']
found_user = users.find_one({'SchülerName': normalized}) found_user = users.find_one({'SchülerName': normalized})
client.close() client.close()
# Do not call dp.decrypt_text() here because found_user is a MongoDB dictionary.
return found_user return found_user
def make_admin(username): def make_admin(username):
""" """Grant administrator privileges to a user."""
Grant administrator privileges to a user.
Args:
username (str): Username of the user to promote
Returns:
bool: True if user was promoted successfully
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one({'Username': username}, {'$set': {'Admin': True}}) result = users.update_one({'Username': dp.encrypt_text(username)}, {'$set': {'Admin': True}})
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one({'username': username}, {'$set': {'Admin': True}}) result = users.update_one({'username': dp.encrypt_text(username)}, {'$set': {'Admin': True}})
client.close() client.close()
return result.matched_count > 0 return result.matched_count > 0
def remove_admin(username): def remove_admin(username):
""" """Remove administrator privileges from a user."""
Remove administrator privileges from a user.
Args:
username (str): Username of the user to demote
Returns:
bool: True if user was demoted successfully
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one({'Username': username}, {'$set': {'Admin': False}}) result = users.update_one({'Username': dp.encrypt_text(username)}, {'$set': {'Admin': False}})
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one({'username': username}, {'$set': {'Admin': False}}) result = users.update_one({'username': dp.encrypt_text(username)}, {'$set': {'Admin': False}})
client.close() client.close()
return result.matched_count > 0 return result.matched_count > 0
def get_user(username): def get_user(username):
""" """Retrieve a specific user by username."""
Retrieve a specific user by username.
Args:
username (str): Username to search for
Returns:
dict: User document or None if not found
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try: try:
def find_in_db(database_name): def find_in_db(database_name):
db = client[database_name] db = client[database_name]
users = db['users'] users = db['users']
return users.find_one({'Username': username}) or users.find_one({'username': username}) return users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)}) or users.find_one({'username': username}) or users.find_one({'Username': username})
# Try current tenant first when available
tenant_db, tenant_id = _resolve_request_tenant_db() tenant_db, tenant_id = _resolve_request_tenant_db()
if tenant_db: if tenant_db:
user = find_in_db(tenant_db) user = find_in_db(tenant_db)
@@ -681,7 +636,6 @@ def get_user(username):
) )
return None return None
# Fallback to default configured database
user = find_in_db(cfg.MONGODB_DB) user = find_in_db(cfg.MONGODB_DB)
if user: if user:
return user return user
@@ -692,147 +646,89 @@ def get_user(username):
def check_admin(username): def check_admin(username):
""" """Check if a user has administrator privileges."""
Check if a user has administrator privileges.
Args:
username (str): Username to check
Returns:
bool: True if user is an administrator, False otherwise
"""
user = get_user(username) user = get_user(username)
return bool(user and user.get('Admin', False)) return bool(user and user.get('Admin', False))
def update_active_ausleihung(username, id_item, ausleihung): def update_active_ausleihung(username, id_item, ausleihung):
""" """Update a user's active borrowing record."""
Update a user's active borrowing record.
Args:
username (str): Username of the user
id_item (str): ID of the borrowed item
ausleihung (str): ID of the borrowing record
Returns:
bool: True if successful
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
users.update_one({'Username': username}, {'$set': {'active_ausleihung': {'Item': id_item, 'Ausleihung': ausleihung}}})
result = users.update_one(
{'Username': dp.encrypt_text(username)},
{'$set': {'active_ausleihung': {'Item': id_item, 'Ausleihung': ausleihung}}}
)
if result.matched_count == 0:
users.update_one(
{'username': dp.encrypt_text(username)},
{'$set': {'active_ausleihung': {'Item': id_item, 'Ausleihung': ausleihung}}}
)
client.close() client.close()
return True return True
def get_active_ausleihung(username): def get_active_ausleihung(username):
""" """Get a user's active borrowing record."""
Get a user's active borrowing record.
Args:
username (str): Username of the user
Returns:
dict: Active borrowing information or None
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username})
return user['active_ausleihung'] user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
client.close()
if not user:
return None
return user.get('active_ausleihung')
def has_active_borrowing(username): def has_active_borrowing(username):
""" """Check if a user currently has an active borrowing."""
Check if a user currently has an active borrowing.
Args:
username (str): Username to check
Returns:
bool: True if user has an active borrowing, False otherwise
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'username': username}) user = users.find_one({'username': dp.encrypt_text(username)}) or users.find_one({'Username': dp.encrypt_text(username)})
if not user:
user = users.find_one({'Username': username})
if not user:
client.close()
return False
has_active = user.get('active_borrowing', False)
client.close() client.close()
return has_active
if not user:
return False
return user.get('active_borrowing', False)
except Exception as e: except Exception as e:
return False return False
def delete_user(username): def delete_user(username):
""" """Delete a user from the database."""
Delete a user from the database.
Administrative function for removing user accounts.
Args:
username (str): Username of the account to delete
Returns:
bool: True if user was deleted successfully, False otherwise
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.delete_one({'username': username})
client.close() result = users.delete_one({'username': dp.encrypt_text(username)})
if result.deleted_count == 0: if result.deleted_count == 0:
# Try with different field name result = users.delete_one({'Username': dp.encrypt_text(username)})
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) client.close()
users = db['users']
result = users.delete_one({'Username': username})
client.close()
return result.deleted_count > 0 return result.deleted_count > 0
def update_active_borrowing(username, item_id, status): def update_active_borrowing(username, item_id, status):
""" """Update a user's active borrowing status."""
Update a user's active borrowing status.
Args:
username (str): Username of the user
item_id (str): ID of the borrowed item or None if returning
status (bool): True if borrowing, False if returning
Returns:
bool: True if successful, False on error
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one(
{'username': username}, update_data = {'$set': {'active_borrowing': status, 'borrowed_item': item_id if status else None}}
{'$set': {
'active_borrowing': status, result = users.update_one({'username': dp.encrypt_text(username)}, update_data)
'borrowed_item': item_id if status else None
}}
)
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one( result = users.update_one({'Username': dp.encrypt_text(username)}, update_data)
{'Username': username},
{'$set': {
'active_borrowing': status,
'borrowed_item': item_id if status else None
}}
)
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
@@ -840,43 +736,37 @@ def update_active_borrowing(username, item_id, status):
def get_name(username): def get_name(username):
""" """Retrieve the name that is associated with the username."""
Retrieve the name that is assosiated with the username.
Returns:
str: String of name
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username})
name = user.get("name") user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
return name client.close()
if not user or not user.get("name"):
return ""
return dp.decrypt_text(user.get("name"))
def get_last_name(username): def get_last_name(username):
""" """Retrieve the last_name that is associated with the username."""
Retrieve the last_name that is assosiated with the username.
Returns:
str: String of last_name
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username})
name = user.get("last_name") user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
return name client.close()
if not user or not user.get("last_name"):
return ""
return dp.decrypt_text(user.get("last_name"))
def get_all_users(): def get_all_users():
""" """Retrieve all users from the database."""
Retrieve all users from the database.
Administrative function for user management.
Returns:
list: List of all user documents
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
@@ -887,65 +777,58 @@ def get_all_users():
except Exception as e: except Exception as e:
return [] return []
def update_password(username, new_password): def update_password(username, new_password):
""" """Update a user's password with a new one."""
Update a user's password with a new one.
Args:
username (str): Username of the user
new_password (str): New password to set
Returns:
bool: True if password was updated successfully, False otherwise
"""
try: try:
if not check_password_strength(new_password): if not check_password_strength(new_password):
return False return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
# Hash the new password
hashed_password = hashing(new_password) hashed_password = hashing(new_password)
# Update the user's password
result = users.update_one( result = users.update_one(
{'Username': username}, {'Username': dp.encrypt_text(username)},
{'$set': {'Password': hashed_password}} {'$set': {'Password': hashed_password}}
) )
if result.matched_count == 0:
result = users.update_one(
{'username': dp.encrypt_text(username)},
{'$set': {'Password': hashed_password}}
)
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
print(f"Error updating password: {e}") print(f"Error updating password: {e}")
return False return False
def update_user_name(username, name, last_name): def update_user_name(username, name, last_name):
""" """Update a user's name and last name."""
Update a user's name and last name.
Args:
username (str): Username of the user
name (str): New first name
last_name (str): New last name
Returns:
bool: True if updated successfully, False otherwise
"""
try: try:
name_alias = build_name_synonym(name, last_name)
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
safe_name = dp.encrypt_text(name.strip()) if name else ''
safe_last_name = dp.encrypt_text(last_name.strip()) if last_name else ''
result = users.update_one( result = users.update_one(
{'Username': username}, {'Username': dp.encrypt_text(username)},
{'$set': {'name': name_alias, 'last_name': ''}} {'$set': {'name': safe_name, 'last_name': safe_last_name}}
) )
if result.matched_count == 0:
result = users.update_one(
{'username': dp.encrypt_text(username)},
{'$set': {'name': safe_name, 'last_name': safe_last_name}}
)
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
print(f"Error updating user name: {e}") print(f"Error updating user name: {e}")
return False return False
+2 -2
View File
@@ -37,14 +37,14 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
<tr> <tr>
<td> <td>
<p style="margin:0 0 12px 0;">Mit freundlichen Grüßen</p> <p style="margin:0 0 12px 0;">Mit freundlichen Grüßen</p>
<p style="margin:0;"><strong style="font-size:16px;">Automatisierter Email Verteiler für die Schule: {cfg.get_school_info().get("name")}</strong><br></p> <p style="margin:0;"><strong style="font-size:16px;">Automatisierter Email Verteiler für die Schule: {cfg.get_school_info().get("name")}</strong><br></p><br>
<p style="margin:12px 0 0 0;"><strong>Invario UG</strong><br>Am Sportplatz 10<br>83052 Bruckmühl</p> <p style="margin:12px 0 0 0;"><strong>Invario UG</strong><br>Am Sportplatz 10<br>83052 Bruckmühl</p>
</td> </td>
</tr> </tr>
</table> </table>
""" """
text_content = f"{body_message}\n\nMit freundlichen Grüßen\n{sender}\nInvario UG" text_content = f"{body_message}\n\nMit freundlichen Grüßen\n{sender}\n"
html_content = f""" html_content = f"""
<html> <html>
+2 -2
View File
@@ -252,8 +252,8 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
if calendar_link: if calendar_link:
email_body += f"\n\nKalendereintrag: {calendar_link}" email_body += f"\n\nKalendereintrag: {calendar_link}"
if normalized_mail and cfg.EMAIL_ENABLED: #if normalized_mail and cfg.EMAIL_ENABLED:
mail_service.send(normalized_mail, subject, email_body) mail_service.send(normalized_mail, subject, email_body, f"Terminplanungssystem {cfg.SCHOOL_INFO_DEFAULT.get("name")}")
return { return {
'appointment_id': id_str, 'appointment_id': id_str,
+80 -58
View File
@@ -20,40 +20,53 @@ logger = logging.getLogger(__name__)
# VAPID keys for push notifications # VAPID keys for push notifications
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '') VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '')
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '') VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '')
VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', f'mailto:admin@{os.getenv("SERVER_NAME", "localhost")}') VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', f'mailto:support@invario-software.de')
# VAPID keys file paths
VAPID_PRIVATE_PEM = os.path.join(os.path.dirname(__file__), 'vapid_private.pem') VAPID_PRIVATE_PEM = os.path.join(os.path.dirname(__file__), 'vapid_private.pem')
VAPID_PUBLIC_PEM = os.path.join(os.path.dirname(__file__), 'vapid_public.pem') VAPID_PUBLIC_PEM = os.path.join(os.path.dirname(__file__), 'vapid_public.pem')
# Auto-generate VAPID keys if none are provided # Load or auto-generate VAPID keys
if not VAPID_PUBLIC_KEY or not VAPID_PRIVATE_KEY: try:
try: from py_vapid import Vapid, b64urlencode
from py_vapid import Vapid, b64urlencode from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import serialization
if not os.path.exists(VAPID_PRIVATE_PEM) or not os.path.exists(VAPID_PUBLIC_PEM):
vapid = Vapid() vapid = Vapid()
if not os.path.exists(VAPID_PRIVATE_PEM): vapid.generate_keys()
vapid.generate_keys() vapid.save_key(VAPID_PRIVATE_PEM)
vapid.save_key(VAPID_PRIVATE_PEM) vapid.save_public_key(VAPID_PUBLIC_PEM)
vapid.save_public_key(VAPID_PUBLIC_PEM) logger.info("Auto-generated new VAPID keys")
logger.info("Auto-generated new VAPID keys") else:
else: vapid = Vapid.from_file(VAPID_PRIVATE_PEM)
vapid = Vapid.from_file(VAPID_PRIVATE_PEM)
raw_pub = vapid.public_key.public_bytes(
raw_pub = vapid.public_key.public_bytes( serialization.Encoding.X962,
serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
serialization.PublicFormat.UncompressedPoint )
)
encoded_pub = b64urlencode(raw_pub)
VAPID_PUBLIC_KEY = b64urlencode(raw_pub).decode('utf-8') if isinstance(encoded_pub, bytes):
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM VAPID_PUBLIC_KEY = encoded_pub.decode('utf-8')
except Exception as e: else:
logger.error(f'Could not load or generate VAPID keys: {e}') VAPID_PUBLIC_KEY = encoded_pub
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM
except Exception as e:
logger.error(f'Could not load or generate VAPID keys: {e}')
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '')
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '')
VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', 'mailto:support@invario-software.de')
# Push service endpoint (typically Firebase or Web Push Service) # Push service endpoint (typically Firebase or Web Push Service)
FCM_API_KEY = os.getenv('FCM_API_KEY', '') # Firebase API key FCM_API_KEY = os.getenv('FCM_API_KEY', '')
def _get_vapid_public():
return VAPID_PUBLIC_KEY
def _get_username_hash(username): def _get_username_hash(username):
"""Generates a deterministic hash for database lookups.""" """Generates a deterministic hash for database lookups."""
if not username: if not username:
@@ -109,29 +122,33 @@ def get_user_subscriptions(username):
def save_push_subscription(username, subscription_obj): def save_push_subscription(username, subscription_obj):
""" import traceback # Hilft uns, Fehler genau zu sehen
Save a new push subscription for a user with field-level encryption.
"""
try: try:
print("--- DEBUG PUSH PAYLOAD ---")
print(subscription_obj)
endpoint = subscription_obj.get('endpoint') endpoint = subscription_obj.get('endpoint')
if not endpoint: keys = subscription_obj.get('keys', {})
logger.warning('Invalid subscription object: missing endpoint')
if not endpoint or not keys.get('p256dh') or not keys.get('auth'):
print(
f"DEBUG FEHLER: Keys fehlen! Endpoint: {bool(endpoint)}, p256dh: {bool(keys.get('p256dh'))}, auth: {bool(keys.get('auth'))}")
return False return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db) subs_col = get_push_subscriptions_collection(db)
# Create unique hash of subscription using plaintext data to avoid duplicates # Create unique hash of subscription using plaintext data to avoid duplicates
sub_hash = hashlib.shake_256( sub_hash = hashlib.shake_256(
f"{username}:{endpoint}".encode('utf-8') f"{username}:{endpoint}".encode('utf-8')
).hexdigest() ).hexdigest(32)
# Check if subscription already exists by Hash # Check if subscription already exists by Hash
existing = subs_col.find_one({ existing = subs_col.find_one({
'SubscriptionHash': sub_hash 'SubscriptionHash': sub_hash
}) })
if existing: if existing:
subs_col.update_one( subs_col.update_one(
{'_id': existing['_id']}, {'_id': existing['_id']},
@@ -143,10 +160,10 @@ def save_push_subscription(username, subscription_obj):
logger.info('Updated existing push subscription') logger.info('Updated existing push subscription')
client.close() client.close()
return True return True
# Format keys as JSON string for your encrypt_text module # Format keys as JSON string for your encrypt_text module
keys_str = json.dumps(subscription_obj.get('keys', {})) keys_str = json.dumps(subscription_obj.get('keys', {}))
# Save new subscription, encrypting sensitive fields # Save new subscription, encrypting sensitive fields
subscription_doc = { subscription_doc = {
'UsernameHash': _get_username_hash(username), 'UsernameHash': _get_username_hash(username),
@@ -159,39 +176,37 @@ def save_push_subscription(username, subscription_obj):
'LastUsed': datetime.datetime.now(), 'LastUsed': datetime.datetime.now(),
'UserAgent': subscription_obj.get('userAgent', ''), 'UserAgent': subscription_obj.get('userAgent', ''),
} }
subs_col.insert_one(subscription_doc) subs_col.insert_one(subscription_doc)
logger.info('Saved new encrypted push subscription') logger.info('Saved new encrypted push subscription')
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
logger.error(f'Error saving push subscription: {e}') print(f"DEBUG ABSTURZ in save_push_subscription: {e}")
traceback.print_exc()
return False return False
def remove_push_subscription(username, endpoint): def remove_push_subscription(username, endpoint):
"""
Remove a push subscription by making it inactive.
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db) subs_col = get_push_subscriptions_collection(db)
# Recreate the deterministic hash to find the specific subscription
sub_hash = hashlib.shake_256( sub_hash = hashlib.shake_256(
f"{username}:{endpoint}".encode('utf-8') f"{username}:{endpoint}".encode('utf-8')
).hexdigest() ).hexdigest(32)
result = subs_col.update_one( result = subs_col.update_one(
{'SubscriptionHash': sub_hash}, {'SubscriptionHash': sub_hash},
{'$set': {'IsActive': False}} {'$set': {'IsActive': False}}
) )
client.close() client.close()
return result.modified_count > 0 return result.matched_count > 0
except Exception as e: except Exception as e:
logger.error(f'Error removing push subscription: {e}') logger.error(f'Error removing push subscription: {e}')
return False return False
@@ -299,8 +314,8 @@ def _send_fcm_notification(subscription, payload):
def _send_web_push_notification(subscription, payload): def _send_web_push_notification(subscription, payload):
try: try:
from pywebpush import webpush from pywebpush import webpush, WebPushException
webpush( webpush(
subscription_info={ subscription_info={
'endpoint': subscription['Endpoint'], 'endpoint': subscription['Endpoint'],
@@ -310,18 +325,25 @@ def _send_web_push_notification(subscription, payload):
vapid_private_key=VAPID_PRIVATE_KEY, vapid_private_key=VAPID_PRIVATE_KEY,
vapid_claims={'sub': VAPID_SUBJECT}, vapid_claims={'sub': VAPID_SUBJECT},
timeout=10, timeout=10,
ttl=3600 ttl=3600
) )
return True return True
except ImportError: except ImportError:
logger.warning('pywebpush not installed. pip install pywebpush') logger.warning('pywebpush not installed. pip install pywebpush')
return False return False
except Exception as e: # HÄRTUNG: Spezifische WebPush-Fehler abfangen
logger.error(f'Web push error: {e}') except WebPushException as ex:
return False # HTTP 404 (Not Found) oder 410 (Gone) bedeuten: Abo existiert nicht mehr
if ex.response is not None and ex.response.status_code in [404, 410]:
logger.info(f"Subscription expired or revoked (Code {ex.response.status_code}). Marking inactive.")
return False # False signalisiert der übergeordneten Funktion, das Abo zu deaktivieren
logger.error(f'Web push failed with code {ex.response.status_code if ex.response else "Unknown"}: {ex}')
return False
except Exception as e:
logger.error(f'Unexpected Web push error: {e}')
return False
def _mark_subscription_inactive(subscription_id): def _mark_subscription_inactive(subscription_id):
try: try:
+2 -1
View File
@@ -16,4 +16,5 @@ openpyxl
cryptography>=42.0.0 cryptography>=42.0.0
pywebpush pywebpush
py-vapid>=1.9.0 py-vapid>=1.9.0
beautifulsoup4 beautifulsoup4
pywebpush
+26 -17
View File
@@ -25,7 +25,7 @@ class PushNotificationManager {
* Initialize push notification system * Initialize push notification system
* Must be called after page load * Must be called after page load
*/ */
async init() { async init(providedKey = null) {
if (!this.isSupported) { if (!this.isSupported) {
console.log('Push notifications not supported in this browser'); console.log('Push notifications not supported in this browser');
return false; return false;
@@ -49,11 +49,18 @@ class PushNotificationManager {
} }
} }
// Fetch VAPID public key from server // Wenn der Key direkt aus dem Template übergeben wurde, nutze diesen (spart einen Request)
if (providedKey) {
this.vapidKey = providedKey;
return true;
}
// Ansonsten: Fetch VAPID public key from server
const keyResponse = await fetch('/api/push/vapid-key'); const keyResponse = await fetch('/api/push/vapid-key');
if (keyResponse.ok) { if (keyResponse.ok) {
const keyData = await keyResponse.json(); const keyData = await keyResponse.json();
this.vapidKey = keyData.vapid_key; // WICHTIG: Muss exakt mit dem Python-JSON-Key übereinstimmen!
this.vapidKey = keyData.publicKey;
} else { } else {
console.warn('Failed to fetch VAPID key'); console.warn('Failed to fetch VAPID key');
return false; return false;
@@ -154,20 +161,22 @@ class PushNotificationManager {
try { try {
const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription(); const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
if (!subscription) { if (!subscription) {
console.warn('No active push subscription');
return false;
}
// Remove subscription on server
const success = await this.removeSubscriptionFromServer(subscription);
// Unsubscribe from push service
if (success) {
await subscription.unsubscribe();
return true; return true;
} }
return false; // Best-Effort: Server benachrichtigen (Eigener try/catch Block!)
try {
await this.removeSubscriptionFromServer(subscription);
} catch (serverError) {
// Fehler vom Server ignorieren wir absichtlich.
// Das Skript läuft weiter, statt hier abzubrechen!
console.warn('Server-Abmeldung fehlgeschlagen, lösche lokal trotzdem:', serverError);
}
// WICHTIG: Das hier wird jetzt garantiert ausgeführt
await subscription.unsubscribe();
return true;
} catch (error) { } catch (error) {
console.error('Failed to unsubscribe from push notifications:', error); console.error('Failed to unsubscribe from push notifications:', error);
return false; return false;
@@ -324,7 +333,7 @@ const pushNotificationManager = new PushNotificationManager();
/** /**
* Show notification subscription UI (typically in settings) * Show notification subscription UI (typically in settings)
*/ */
function showPushNotificationSettings() { function showPushNotificationSettings(vapidPublicKey) {
const container = document.getElementById('push-notification-settings'); const container = document.getElementById('push-notification-settings');
if (!container) return; if (!container) return;
@@ -350,9 +359,9 @@ function showPushNotificationSettings() {
container.innerHTML = html; container.innerHTML = html;
// Set up button handler // Set up button handler and pass the VAPID public key to init()
const toggleBtn = document.getElementById('toggle-push-btn'); const toggleBtn = document.getElementById('toggle-push-btn');
pushNotificationManager.init().then(() => { pushNotificationManager.init(vapidPublicKey).then(() => {
updatePushStatus(); updatePushStatus();
}); });
+14 -3
View File
@@ -7,7 +7,7 @@
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap; margin-bottom:16px;"> <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap; margin-bottom:16px;">
<div> <div>
<h1 style="margin:0;">Defekte Items</h1> <h1 style="margin:0;">Defekte Items</h1>
<p style="margin:6px 0 0; color:#64748b;">Eigenes Verwaltungsfenster fuer gemeldete Defekte mit schneller Reparatur-Funktion.</p> <p style="margin:6px 0 0; color:#64748b;">Eigenes Verwaltungsfenster für gemeldete Defekte mit schneller Reparatur-Funktion.</p>
</div> </div>
<div style="display:flex; gap:8px; flex-wrap:wrap;"> <div style="display:flex; gap:8px; flex-wrap:wrap;">
<a class="btn btn-outline-secondary" href="{{ url_for('admin_borrowings') }}">Ausleihen</a> <a class="btn btn-outline-secondary" href="{{ url_for('admin_borrowings') }}">Ausleihen</a>
@@ -42,7 +42,13 @@
{% if item.isbn %}<div style="font-size:0.82rem; color:#64748b;">ISBN: {{ item.isbn }}</div>{% endif %} {% if item.isbn %}<div style="font-size:0.82rem; color:#64748b;">ISBN: {{ item.isbn }}</div>{% endif %}
</td> </td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;"> <td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<div style="display:inline-block; padding:3px 9px; border-radius:999px; background:#fee2e2; color:#991b1b; font-size:0.75rem; font-weight:700;">Defekt</div>
<!-- HIER IST DIE ÄNDERUNG: Dynamischer Status-Badge -->
<div class="badge bg-{{ item.status_color }} rounded-pill" style="font-size:0.75rem; font-weight:700; padding:4px 10px;">
{{ item.status_text }}
</div>
<!-- ENDE DER ÄNDERUNG -->
<div style="font-size:0.84rem; color:#475569; margin-top:6px;">Meldungen: {{ item.damage_count }}</div> <div style="font-size:0.84rem; color:#475569; margin-top:6px;">Meldungen: {{ item.damage_count }}</div>
<div style="font-size:0.84rem; color:#475569;">Condition: {{ item.condition or '-' }}</div> <div style="font-size:0.84rem; color:#475569;">Condition: {{ item.condition or '-' }}</div>
<div style="font-size:0.84rem; color:#475569;">Verfuegbar: {{ 'Ja' if item.available else 'Nein' }}</div> <div style="font-size:0.84rem; color:#475569;">Verfuegbar: {{ 'Ja' if item.available else 'Nein' }}</div>
@@ -66,7 +72,12 @@
{% endif %} {% endif %}
</td> </td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;"> <td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<!-- Button wird ausgeblendet, falls das Item bereits repariert ist -->
{% if item.condition not in ['repaired', 'fixed'] %}
<button class="btn btn-success btn-sm" onclick="repairDamage('{{ item.id }}', this)">Als repariert markieren</button> <button class="btn btn-success btn-sm" onclick="repairDamage('{{ item.id }}', this)">Als repariert markieren</button>
{% else %}
<span class="text-success" style="font-size: 0.85rem;"><i class="bi bi-check-circle"></i> Repariert</span>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -118,4 +129,4 @@ function repairDamage(itemId, button) {
}); });
} }
</script> </script>
{% endblock %} {% endblock %}
+5 -5
View File
@@ -1148,7 +1148,7 @@
{% if current_permissions.pages.get('tutorial_page', False) %} {% if current_permissions.pages.get('tutorial_page', False) %}
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li> <li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %} {% endif %}
{% if current_permissions.actions.get('can_view_logs', False) or current_permissions.pages.get('admin_audit_dashboard', False) %} {% if current_permissions.actions.get('can_view_logs', False) or current_permissions.pages.get('admin_audit_dashboard', False) %}
@@ -1257,7 +1257,7 @@
{% if current_permissions.pages.get('manage_locations', False) %} {% if current_permissions.pages.get('manage_locations', False) %}
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_borrowings', False) %} {% if current_permissions.pages.get('admin_borrowings', False) %}
@@ -1379,7 +1379,7 @@
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li> <li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1698,7 +1698,7 @@
<option value="Orte verwalten"></option> <option value="Orte verwalten"></option>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<option value="Schulstammdaten"></option> <option value="Schulstammdaten"></option>
{% endif %} {% endif %}
@@ -1823,7 +1823,7 @@
{ label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} }, { label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} },
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
{ label: 'Schulstammdaten', keywords: ['schule', 'settings', 'stammdaten', 'school settings'], url: {{ url_for('admin_school_settings')|tojson }} }, { label: 'Schulstammdaten', keywords: ['schule', 'settings', 'stammdaten', 'school settings'], url: {{ url_for('admin_school_settings')|tojson }} },
{% endif %} {% endif %}
+1 -1
View File
@@ -1371,8 +1371,8 @@
await loadLibraryItems(); // Daten neu laden await loadLibraryItems(); // Daten neu laden
// renderTable(); // Ggf. Tabelle neu rendern // renderTable(); // Ggf. Tabelle neu rendern
} else { } else {
closeEditLibraryModal();
await loadLibraryItems(); await loadLibraryItems();
closeEditLibraryModal();
} }
} catch (error) { } catch (error) {
console.error('Update failed:', error); console.error('Update failed:', error);
+1 -1
View File
@@ -89,7 +89,7 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (typeof showPushNotificationSettings === 'function') { if (typeof showPushNotificationSettings === 'function') {
showPushNotificationSettings(); showPushNotificationSettings('{{ vapid_public_key }}');
} }
}); });
</script> </script>
+2 -30
View File
@@ -82,7 +82,6 @@ while i < len(lines):
stripped = line.lstrip(" ") stripped = line.lstrip(" ")
indent = leading_spaces(line) indent = leading_spaces(line)
# Check if we're starting the app service
if not in_app_service and re.match(r"^\s*app:\s*$", line): if not in_app_service and re.match(r"^\s*app:\s*$", line):
in_app_service = True in_app_service = True
app_indent = indent app_indent = indent
@@ -92,9 +91,7 @@ while i < len(lines):
i += 1 i += 1
continue continue
# Check if we've exited the app service (found another top-level service)
if in_app_service and indent <= app_indent and re.match(r"^[A-Za-z0-9_-]+:\s*$", stripped): if in_app_service and indent <= app_indent and re.match(r"^[A-Za-z0-9_-]+:\s*$", stripped):
# We've left the app service - insert image if we haven't already
if build_found and app_service_indent is not None: if build_found and app_service_indent is not None:
out.append(f"{' ' * app_service_indent}image: {target_image}\n") out.append(f"{' ' * app_service_indent}image: {target_image}\n")
in_app_service = False in_app_service = False
@@ -102,24 +99,20 @@ while i < len(lines):
i += 1 i += 1
continue continue
# Process lines within the app service
if in_app_service: if in_app_service:
if app_service_indent is None and indent > app_indent: if app_service_indent is None and indent > app_indent:
app_service_indent = indent app_service_indent = indent
# Check for image key (already has an image, don't add)
if re.match(rf"^\s+image:\s*", line): if re.match(rf"^\s+image:\s*", line):
in_app_service = False in_app_service = False
out.append(line) out.append(line)
i += 1 i += 1
continue continue
# Check for build block
if re.match(rf"^\s+build:\s*$", line): if re.match(rf"^\s+build:\s*$", line):
build_found = True build_found = True
out.append(line) out.append(line)
i += 1 i += 1
# Skip all lines that are part of the build block (indented more than app_service_indent)
while i < len(lines): while i < len(lines):
next_line = lines[i] next_line = lines[i]
next_indent = leading_spaces(next_line) next_indent = leading_spaces(next_line)
@@ -130,12 +123,10 @@ while i < len(lines):
break break
continue continue
# Insert image after build block before first property
if build_found and app_service_indent is not None and indent == app_service_indent: if build_found and app_service_indent is not None and indent == app_service_indent:
# Check if this is a property line (not build)
if not re.match(rf"^\s+build:", line): if not re.match(rf"^\s+build:", line):
out.append(f"{' ' * app_service_indent}image: {target_image}\n") out.append(f"{' ' * app_service_indent}image: {target_image}\n")
build_found = False # Mark that we've inserted the image build_found = False
out.append(line) out.append(line)
i += 1 i += 1
@@ -144,7 +135,6 @@ while i < len(lines):
out.append(line) out.append(line)
i += 1 i += 1
# If we ended while still in the app service, append image at the end
if in_app_service and build_found and app_service_indent is not None: if in_app_service and build_found and app_service_indent is not None:
out.append(f"{' ' * app_service_indent}image: {target_image}\n") out.append(f"{' ' * app_service_indent}image: {target_image}\n")
@@ -377,18 +367,12 @@ with open(meta_file, 'r', encoding='utf-8') as f:
data = json.load(f) data = json.load(f)
tag = data.get('tag_name', '').strip() tag = data.get('tag_name', '').strip()
url = '' url = ''
image_url = ''
for asset in data.get('assets', []): for asset in data.get('assets', []):
if asset.get('name') == asset_name: if asset.get('name') == asset_name:
url = asset.get('browser_download_url', '').strip() url = asset.get('browser_download_url', '').strip()
break break
for asset in data.get('assets', []):
if asset.get('name') == f'inventarsystem-image-{tag}.tar.gz':
image_url = asset.get('browser_download_url', '').strip()
break
print(tag) print(tag)
print(url) print(url)
print(image_url)
PY PY
} }
@@ -401,7 +385,7 @@ main() {
need_cmd python3 need_cmd python3
need_cmd curl need_cmd curl
local meta_file tag bundle_url image_url local meta_file tag bundle_url
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
meta_file="$TMP_DIR/release.json" meta_file="$TMP_DIR/release.json"
trap cleanup_tmp_dir EXIT trap cleanup_tmp_dir EXIT
@@ -409,7 +393,6 @@ main() {
mapfile -t release_info < <(latest_tag_and_bundle_url "$meta_file") mapfile -t release_info < <(latest_tag_and_bundle_url "$meta_file")
tag="${release_info[0]:-}" tag="${release_info[0]:-}"
bundle_url="${release_info[1]:-}" bundle_url="${release_info[1]:-}"
image_url="${release_info[2]:-}"
if [ -z "$tag" ] || [ -z "$bundle_url" ]; then if [ -z "$tag" ] || [ -z "$bundle_url" ]; then
echo "Error: latest release metadata is incomplete." echo "Error: latest release metadata is incomplete."
@@ -425,17 +408,6 @@ main() {
pin_compose_app_image "$tag" pin_compose_app_image "$tag"
if [ -z "$image_url" ]; then
echo "Error: release image asset is missing"
exit 1
fi
curl -fL "$image_url" -o "$TMP_DIR/inventarsystem-image-$tag.tar.gz"
sudo docker load -i "$TMP_DIR/inventarsystem-image-$tag.tar.gz" >/dev/null
# Tagge das geladene Gitea-Image als latest
sudo docker tag "git.invario-software.eu/invario/inventarsystem:$tag" "git.invario-software.eu/invario/inventarsystem:latest" >/dev/null 2>&1 || true
if [ ! -f "$PROJECT_DIR/start.sh" ]; then if [ ! -f "$PROJECT_DIR/start.sh" ]; then
echo "Error: release bundle is missing start.sh" echo "Error: release bundle is missing start.sh"
exit 1 exit 1
+5 -4
View File
@@ -200,6 +200,7 @@ sys.path.insert(0, "/app")
sys.path.insert(0, "/app/Web") sys.path.insert(0, "/app/Web")
from Web.modules.database import settings from Web.modules.database import settings
from pymongo import MongoClient from pymongo import MongoClient
import Web.modules.inventarsystem.data_protection as dp
tenant_id = sys.argv[1].lower() tenant_id = sys.argv[1].lower()
mode = sys.argv[2] mode = sys.argv[2]
@@ -244,14 +245,14 @@ page_permissions = {
"manage_locations": True, "manage_locations": True,
} }
if db.users.count_documents({"Username": "admin"}) == 0: if db.users.count_documents({"Username": dp.encrypt_text("admin")}) == 0:
db.users.insert_one({ db.users.insert_one({
"Username": "admin", "Username": dp.encrypt_text("admin"),
"Password": hashed_pw_string, "Password": hashed_pw_string,
"Admin": True, "Admin": True,
"active_ausleihung": None, "active_ausleihung": None,
"name": "Admin", "name": dp.encrypt_text("Admin"),
"last_name": "User", "last_name": dp.encrypt_text("User"),
"IsStudent": False, "IsStudent": False,
"PermissionPreset": "full_access", "PermissionPreset": "full_access",
"ActionPermissions": action_permissions, "ActionPermissions": action_permissions,
+2 -1
View File
@@ -16,4 +16,5 @@ openpyxl
cryptography>=42.0.0 cryptography>=42.0.0
pywebpush pywebpush
py-vapid>=1.9.0 py-vapid>=1.9.0
beautifulsoup4 beautifulsoup4
pywebpush
+7 -44
View File
@@ -6,7 +6,6 @@ cd "$SCRIPT_DIR"
ENV_FILE="$SCRIPT_DIR/.docker-build.env" ENV_FILE="$SCRIPT_DIR/.docker-build.env"
APP_IMAGE_REPO="git.invario-software.eu/invario/inventarsystem" APP_IMAGE_REPO="git.invario-software.eu/invario/inventarsystem"
DIST_DIR="$SCRIPT_DIR/dist"
RUNTIME_COMPOSE_OVERRIDE_FILE="$SCRIPT_DIR/.docker-compose.runtime.override.yml" RUNTIME_COMPOSE_OVERRIDE_FILE="$SCRIPT_DIR/.docker-compose.runtime.override.yml"
SUDO="" SUDO=""
@@ -279,52 +278,19 @@ EOF
fi fi
} }
ensure_app_image_loaded() { ensure_app_image_ready() {
if docker image inspect "$APP_IMAGE_VALUE" >/dev/null 2>&1; then if docker image inspect "$APP_IMAGE_VALUE" >/dev/null 2>&1; then
return 0 return 0
fi fi
local image_archive echo "Attempting to pull registry image: $APP_IMAGE_VALUE"
image_archive="$(find_local_dist_image_archive || true)" if docker pull "$APP_IMAGE_VALUE" >/dev/null 2>&1; then
if [ -n "$image_archive" ]; then
echo "Loading app image from local dist artifact: $image_archive"
if docker load -i "$image_archive" >/dev/null 2>&1 && docker image inspect "$APP_IMAGE_VALUE" >/dev/null 2>&1; then
return 0
fi
echo "Warning: failed to load expected app image from $image_archive"
fi
echo "Error: local app image not found: $APP_IMAGE_VALUE"
echo "Run ./update.sh so the nightly updater loads the release image first."
exit 1
}
find_local_dist_image_archive() {
local tag archive
if [ ! -d "$DIST_DIR" ]; then
return 1
fi
tag="${APP_IMAGE_VALUE##*:}"
for archive in \
"$DIST_DIR/inventarsystem-image-$tag.tar.gz" \
"$DIST_DIR/inventarsystem-image-$tag.tar" \
"$DIST_DIR/inventarsystem-image.tar.gz" \
"$DIST_DIR/inventarsystem-image.tar"; do
if [ -f "$archive" ]; then
echo "$archive"
return 0
fi
done
archive="$(find "$DIST_DIR" -maxdepth 1 -type f \( -name 'inventarsystem-image-*.tar.gz' -o -name 'inventarsystem-image-*.tar' \) | sort | tail -n1)"
if [ -n "$archive" ]; then
echo "$archive"
return 0 return 0
fi fi
return 1 echo "Error: app image not found locally and pull failed: $APP_IMAGE_VALUE"
echo "Run ./update.sh to pull the latest release image from the registry."
exit 1
} }
configure_nuitka_mode() { configure_nuitka_mode() {
@@ -605,7 +571,6 @@ verify_stack_health() {
fi fi
compose_args+=(--env-file "$ENV_FILE") compose_args+=(--env-file "$ENV_FILE")
# Try health check with optional restart on first failure
while [[ $retry_count -lt 2 ]]; do while [[ $retry_count -lt 2 ]]; do
echo "Waiting for containers to become healthy... (attempt $((retry_count + 1))/2)" echo "Waiting for containers to become healthy... (attempt $((retry_count + 1))/2)"
for _ in $(seq 1 60); do for _ in $(seq 1 60); do
@@ -623,7 +588,6 @@ verify_stack_health() {
sleep 2 sleep 2
done done
# First failure: attempt recovery by restarting containers
if [[ $retry_count -eq 0 ]]; then if [[ $retry_count -eq 0 ]]; then
echo "Health check failed. Attempting to restart containers..." echo "Health check failed. Attempting to restart containers..."
docker compose "${compose_args[@]}" ps || true docker compose "${compose_args[@]}" ps || true
@@ -636,7 +600,6 @@ verify_stack_health() {
fi fi
done done
# Final failure
echo "Error: stack health check failed after restart attempt." echo "Error: stack health check failed after restart attempt."
docker compose "${compose_args[@]}" ps || true docker compose "${compose_args[@]}" ps || true
docker compose "${compose_args[@]}" logs --tail=120 app redis mongodb || true docker compose "${compose_args[@]}" logs --tail=120 app redis mongodb || true
@@ -654,7 +617,7 @@ resolve_app_image
configure_host_ports configure_host_ports
ensure_min_docker_disk_space ensure_min_docker_disk_space
detect_server_capacity detect_server_capacity
ensure_app_image_loaded ensure_app_image_ready
write_env_file write_env_file
write_runtime_compose_override write_runtime_compose_override
+31 -151
View File
@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
# Release-only updater for Docker deployment. # Release-only updater for Docker deployment.
# Updates are pulled exclusively from Gitea Releases assets. # Updates pull the deployment bundle from Gitea and the Docker Image via 'docker pull'.
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="$PROJECT_DIR/logs" LOG_DIR="$PROJECT_DIR/logs"
@@ -11,13 +11,10 @@ STATE_FILE="$PROJECT_DIR/.release-version"
REPO_SLUG="Invario/Inventarsystem" REPO_SLUG="Invario/Inventarsystem"
API_URL="https://git.invario-software.eu/api/v1/repos/$REPO_SLUG/releases/latest" API_URL="https://git.invario-software.eu/api/v1/repos/$REPO_SLUG/releases/latest"
BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz" BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz"
APP_IMAGE_ASSET_PREFIX="inventarsystem-image-"
ENV_FILE="$PROJECT_DIR/.docker-build.env" ENV_FILE="$PROJECT_DIR/.docker-build.env"
APP_IMAGE_REPO="git.invario-software.eu/invario/inventarsystem" APP_IMAGE_REPO="git.invario-software.eu/invario/inventarsystem"
DIST_DIR="$PROJECT_DIR/dist"
COMPOSE_FILE="docker-compose-multitenant.yml" COMPOSE_FILE="docker-compose-multitenant.yml"
MIN_ROOT_FREE_MB="${INVENTAR_MIN_ROOT_FREE_MB:-2048}" MIN_ROOT_FREE_MB="${INVENTAR_MIN_ROOT_FREE_MB:-2048}"
DIST_KEEP_COUNT="${INVENTAR_DIST_KEEP_COUNT:-2}"
MODE="release" MODE="release"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
@@ -109,36 +106,6 @@ ensure_min_root_disk_space() {
fi fi
} }
cleanup_old_dist_artifacts() {
local keep_count
keep_count="$DIST_KEEP_COUNT"
if [ ! -d "$DIST_DIR" ]; then
return 0
fi
if ! [[ "$keep_count" =~ ^[0-9]+$ ]]; then
keep_count=2
fi
mapfile -t archives < <(find "$DIST_DIR" -maxdepth 1 -type f \( -name 'inventarsystem-image-*.tar.gz' -o -name 'inventarsystem-image-*.tar' \) -printf '%T@ %p\n' | sort -nr | awk '{print $2}')
if [ "${#archives[@]}" -le "$keep_count" ]; then
return 0
fi
local index old_archive deleted=0
for (( index=keep_count; index<${#archives[@]}; index++ )); do
old_archive="${archives[$index]}"
if rm -f "$old_archive"; then
deleted=$((deleted + 1))
fi
done
if [ "$deleted" -gt 0 ]; then
log_message "Cleaned up $deleted old dist image archive(s)"
fi
}
cleanup_docker_dangling_images() { cleanup_docker_dangling_images() {
if docker image prune -f >> "$LOG_FILE" 2>&1; then if docker image prune -f >> "$LOG_FILE" 2>&1; then
log_message "Cleaned up dangling Docker images" log_message "Cleaned up dangling Docker images"
@@ -153,7 +120,6 @@ Usage: $0 [options]
Options: Options:
--multitenant Use docker-compose-multitenant.yml (default) --multitenant Use docker-compose-multitenant.yml (default)
development Install development build from Gitea Registry or local dist
-h, --help Show this help message -h, --help Show this help message
EOF EOF
} }
@@ -165,10 +131,6 @@ parse_args() {
COMPOSE_FILE="docker-compose-multitenant.yml" COMPOSE_FILE="docker-compose-multitenant.yml"
shift shift
;; ;;
development|dev)
MODE="development"
shift
;;
-h|--help) -h|--help)
usage usage
exit 0 exit 0
@@ -216,14 +178,12 @@ create_backup() {
} }
fetch_release_metadata() { fetch_release_metadata() {
local meta_file local meta_file="$1"
meta_file="$1"
curl -fsSL "$API_URL" -o "$meta_file" curl -fsSL "$API_URL" -o "$meta_file"
} }
parse_latest_tag() { parse_latest_tag() {
local meta_file local meta_file="$1"
meta_file="$1"
python3 - <<'PY' "$meta_file" python3 - <<'PY' "$meta_file"
import json, sys import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f: with open(sys.argv[1], 'r', encoding='utf-8') as f:
@@ -233,9 +193,8 @@ PY
} }
parse_asset_url() { parse_asset_url() {
local meta_file asset_name local meta_file="$1"
meta_file="$1" local asset_name="$2"
asset_name="$2"
python3 - <<'PY' "$meta_file" "$asset_name" python3 - <<'PY' "$meta_file" "$asset_name"
import json, sys import json, sys
meta_file, asset_name = sys.argv[1], sys.argv[2] meta_file, asset_name = sys.argv[1], sys.argv[2]
@@ -248,31 +207,6 @@ for asset in data.get('assets', []):
PY PY
} }
load_release_image() {
local meta_file tag image_asset image_url tmp_dir archive
meta_file="$1"
tag="$2"
image_asset="${APP_IMAGE_ASSET_PREFIX}${tag}.tar.gz"
image_url="$(parse_asset_url "$meta_file" "$image_asset")"
if [ -z "$image_url" ]; then
log_message "ERROR: Release image asset not found: $image_asset"
return 1
fi
tmp_dir="$(mktemp -d)"
archive="$tmp_dir/$image_asset"
trap 'rm -rf "${tmp_dir:-}"' RETURN
log_message "Loading app image from release asset $image_asset"
curl -fL "$image_url" -o "$archive"
docker load -i "$archive" >> "$LOG_FILE" 2>&1
docker tag "$APP_IMAGE_REPO:$tag" "$APP_IMAGE_REPO:latest" >> "$LOG_FILE" 2>&1 || true
trap - RETURN
}
refresh_runtime_scripts_from_main() { refresh_runtime_scripts_from_main() {
local start_url stop_url restart_url update_url local start_url stop_url restart_url update_url
start_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/start.sh" start_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/start.sh"
@@ -285,61 +219,13 @@ refresh_runtime_scripts_from_main() {
curl -fsSL "$restart_url" -o "$PROJECT_DIR/restart.sh" || log_message "WARNING: Could not refresh restart.sh from main" curl -fsSL "$restart_url" -o "$PROJECT_DIR/restart.sh" || log_message "WARNING: Could not refresh restart.sh from main"
curl -fsSL "$update_url" -o "$PROJECT_DIR/update.sh" || log_message "WARNING: Could not refresh update.sh from main" curl -fsSL "$update_url" -o "$PROJECT_DIR/update.sh" || log_message "WARNING: Could not refresh update.sh from main"
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" 2>/dev/null || true
}
find_local_dist_image_archive() {
local tag="$1"
local archive
if [ ! -d "$DIST_DIR" ]; then
return 1
fi
for archive in \
"$DIST_DIR/inventarsystem-image-$tag.tar.gz" \
"$DIST_DIR/inventarsystem-image-$tag.tar" \
"$DIST_DIR/inventarsystem-image.tar.gz" \
"$DIST_DIR/inventarsystem-image.tar"; do
if [ -f "$archive" ]; then
echo "$archive"
return 0
fi
done
archive="$(find "$DIST_DIR" -maxdepth 1 -type f \( -name 'inventarsystem-image-*.tar.gz' -o -name 'inventarsystem-image-*.tar' \) | sort | tail -n1)"
if [ -n "$archive" ]; then
echo "$archive"
return 0
fi
return 1
}
load_local_dist_image() {
local tag="$1"
local archive
archive="$(find_local_dist_image_archive "$tag" || true)"
if [ -z "$archive" ]; then
return 1
fi
log_message "Loading app image from local dist artifact: $archive"
if docker load -i "$archive" >> "$LOG_FILE" 2>&1; then
docker tag "$APP_IMAGE_REPO:$tag" "$APP_IMAGE_REPO:latest" >> "$LOG_FILE" 2>&1 || true
return 0
fi
log_message "WARNING: Failed to load local dist artifact: $archive"
return 1
} }
download_and_extract_bundle() { download_and_extract_bundle() {
local url tmp_dir archive local url="$1"
url="$1" local tmp_dir="$2"
tmp_dir="$2" local archive="$tmp_dir/$BUNDLE_ASSET"
archive="$tmp_dir/$BUNDLE_ASSET"
curl -fL "$url" -o "$archive" curl -fL "$url" -o "$archive"
tar -xzf "$archive" -C "$tmp_dir" tar -xzf "$archive" -C "$tmp_dir"
@@ -375,19 +261,15 @@ download_and_extract_bundle() {
# Ensure executable permissions on all copied scripts # Ensure executable permissions on all copied scripts
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true
chmod +x "$PROJECT_DIR"/manage-tenant.sh "$PROJECT_DIR"/run-tenant-cmd.sh 2>/dev/null || true
if [ ! -f "$PROJECT_DIR/config.json" ] && [ -f "$tmp_dir/config.json" ]; then if [ ! -f "$PROJECT_DIR/config.json" ] && [ -f "$tmp_dir/config.json" ]; then
cp -f "$tmp_dir/config.json" "$PROJECT_DIR/config.json" cp -f "$tmp_dir/config.json" "$PROJECT_DIR/config.json"
log_message "Installed default config.json from release bundle" log_message "Installed default config.json from release bundle"
fi fi
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true
} }
deploy() { deploy() {
local tag="$1" local tag="$1"
local meta_file="$2"
local app_image="${APP_IMAGE_REPO}:${tag}" local app_image="${APP_IMAGE_REPO}:${tag}"
local compose_path local compose_path
@@ -410,17 +292,14 @@ EOF
printf '\nINVENTAR_APP_IMAGE=%s\n' "$app_image" >> "$ENV_FILE" printf '\nINVENTAR_APP_IMAGE=%s\n' "$app_image" >> "$ENV_FILE"
fi fi
if ! load_local_dist_image "$tag"; then log_message "Pulling Gitea Registry image $app_image"
if ! load_release_image "$meta_file" "$tag"; then if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then
log_message "Falling back to tagged Gitea Registry image $app_image" log_message "Falling back to local Docker build for $app_image"
if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then docker build -t "$app_image" "$PROJECT_DIR" >> "$LOG_FILE" 2>&1
log_message "Falling back to local Docker build for $app_image"
docker build -t "$app_image" "$PROJECT_DIR" >> "$LOG_FILE" 2>&1
fi
fi
fi fi
docker compose -f "$compose_path" --env-file "$ENV_FILE" pull app mongodb >> "$LOG_FILE" 2>&1 # Image wurde oben gepullt, Stack hochfahren
docker compose -f "$compose_path" --env-file "$ENV_FILE" pull mongodb redis >> "$LOG_FILE" 2>&1 || true
docker compose -f "$compose_path" --env-file "$ENV_FILE" up -d --remove-orphans >> "$LOG_FILE" 2>&1 docker compose -f "$compose_path" --env-file "$ENV_FILE" up -d --remove-orphans >> "$LOG_FILE" 2>&1
docker tag "$app_image" "$APP_IMAGE_REPO:latest" >> "$LOG_FILE" 2>&1 || true docker tag "$app_image" "$APP_IMAGE_REPO:latest" >> "$LOG_FILE" 2>&1 || true
} }
@@ -460,9 +339,12 @@ cleanup_server_space() {
else else
log_message "WARNING: Docker system prune failed" log_message "WARNING: Docker system prune failed"
fi fi
# Clean up old dist artifacts # Delete legacy dist folder if it exists
cleanup_old_dist_artifacts if [ -d "$PROJECT_DIR/dist" ]; then
# Clean up log files older than 7 days rm -rf "$PROJECT_DIR/dist" || true
log_message "Legacy dist folder removed"
fi
# Clean up old log files older than 7 days
if find "$LOG_DIR" -type f -name '*.log' -mtime +7 -exec rm -f {} +; then if find "$LOG_DIR" -type f -name '*.log' -mtime +7 -exec rm -f {} +; then
log_message "Old log files (older than 7 days) cleaned up" log_message "Old log files (older than 7 days) cleaned up"
else else
@@ -511,17 +393,14 @@ EOF
printf '\nINVENTAR_APP_IMAGE=%s\n' "$app_image" >> "$ENV_FILE" printf '\nINVENTAR_APP_IMAGE=%s\n' "$app_image" >> "$ENV_FILE"
fi fi
# Try local dist first, then pull from Gitea Registry log_message "Attempting to pull development image $app_image"
if ! load_local_dist_image "$tag"; then if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then
log_message "Attempting to pull development image $app_image" log_message "ERROR: Could not obtain development image $app_image"
if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then exit 1
log_message "ERROR: Could not obtain development image $app_image"
exit 1
fi
fi fi
# Bring up stack # Bring up stack
docker compose -f "$compose_path" --env-file "$ENV_FILE" pull app mongodb >> "$LOG_FILE" 2>&1 || true docker compose -f "$compose_path" --env-file "$ENV_FILE" pull mongodb redis >> "$LOG_FILE" 2>&1 || true
docker compose -f "$compose_path" --env-file "$ENV_FILE" up -d --remove-orphans >> "$LOG_FILE" 2>&1 docker compose -f "$compose_path" --env-file "$ENV_FILE" up -d --remove-orphans >> "$LOG_FILE" 2>&1
if ! verify_stack_health; then if ! verify_stack_health; then
@@ -581,7 +460,7 @@ EOF
if [ "$current_tag" = "$latest_tag" ]; then if [ "$current_tag" = "$latest_tag" ]; then
log_message "Already on latest release ($latest_tag). Refreshing containers from prebuilt image." log_message "Already on latest release ($latest_tag). Refreshing containers from prebuilt image."
deploy "$latest_tag" "$meta_file" deploy "$latest_tag"
if verify_stack_health; then if verify_stack_health; then
log_message "Container refresh completed" log_message "Container refresh completed"
else else
@@ -611,18 +490,19 @@ EOF
log_message "Updating from release $latest_tag" log_message "Updating from release $latest_tag"
download_and_extract_bundle "$bundle_url" "$tmp_dir" download_and_extract_bundle "$bundle_url" "$tmp_dir"
refresh_runtime_scripts_from_main refresh_runtime_scripts_from_main
deploy "$latest_tag" "$meta_file" deploy "$latest_tag"
if ! verify_stack_health; then if ! verify_stack_health; then
log_message "ERROR: Updated stack failed health check" log_message "ERROR: Updated stack failed health check"
exit 1 exit 1
fi fi
echo "$latest_tag" > "$STATE_FILE" echo "$latest_tag" > "$STATE_FILE"
cleanup_old_dist_artifacts
cleanup_docker_dangling_images cleanup_docker_dangling_images
log_message "Update completed successfully to release $latest_tag" log_message "Update completed successfully to release $latest_tag"
sudo ./opt/Inventarsystem/restart.sh if [ -x "./opt/Inventarsystem/restart.sh" ]; then
sudo ./opt/Inventarsystem/restart.sh
fi
echo "Restart of the Server Completed" echo "Restart of the Server Completed"
} }