Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87027cf3c9 | |||
| 46f79b002b | |||
| dcc8aae650 | |||
| c9fdf41e0b | |||
| 3e26c691b1 | |||
| 6c3d62e84b | |||
| 88f6c3c0f5 | |||
| 08d8b78d91 | |||
| 258e287a39 | |||
| 9b12d9a3d0 | |||
| 237788af96 | |||
| 7368d82fc4 | |||
| 3e5e243ddf | |||
| 8176cea8fe | |||
| b71f2e9089 | |||
| 1331afa4da | |||
| 5a2d703bc7 | |||
| 8e6dd17243 | |||
| 2124ca65b2 | |||
| bc86dc4a0d | |||
| a49ed98ed7 | |||
| 4d9bb62907 | |||
| 8251cd9bfd | |||
| 3ed3148b8a | |||
| 27b265eaf5 | |||
| 3571bb6f6d | |||
| b037434e89 | |||
| ad14499df0 | |||
| 6f50fb2263 |
@@ -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
@@ -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(
|
||||||
|
|||||||
+333
-688
File diff suppressed because it is too large
Load Diff
+205
-173
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user