Compare commits

..

28 Commits

Author SHA1 Message Date
Aiirondev_dev 70b108d841 Changes 2026-07-27 23:49:30 +02:00
Aiirondev_dev fc53333436 Oke 2026-07-27 23:49:30 +02:00
Aiirondev_dev 33ae7ee1ac removal of unused legal files from old gh repo 2026-07-27 22:42:21 +02:00
Aiirondev_dev 16962b20b6 Add of descryption of for the removal of big tmp files 2026-07-27 21:17:51 +02:00
Aiirondev_dev 6fcabc8638 changes to the open Damage Invoice Modal 2026-07-27 21:13:18 +02:00
Aiirondev_dev 3068f44563 implementet automatic including of the price 2026-07-27 20:16:53 +02:00
Aiirondev_dev c4dd18a2e4 change to have automatic clearing of the tmp directory 2026-07-27 15:30:34 +02:00
Aiirondev_dev d0ac21e7dd changes to the Library Type 2026-07-27 15:20:27 +02:00
Aiirondev_dev b4a505c20b changes to the borrow_record creation 2026-07-27 15:09:26 +02:00
Aiirondev_dev 39302d4d1f CHanges to the In and out 2026-07-27 14:53:09 +02:00
Aiirondev_dev e46f8b0c66 finisch of the borrower funktion 2026-07-27 14:43:39 +02:00
Aiirondev_dev 06486f039f changes for the loading of the recent borrowers that only the latest 3 get displayed 2026-07-27 14:34:59 +02:00
Aiirondev_dev feac00f0df changes to the permission development 2026-07-27 14:19:01 +02:00
Aiirondev_dev 0b0169ef96 made the Cookie Banner accoding to the ePrivacy-Richtlinie/TTDSG only a visual informative banner 2026-07-27 11:35:12 +02:00
Aiirondev_dev a6db3a001f changes to maybe have the right passtword 2026-07-27 00:32:21 +02:00
Aiirondev_dev 8fa495a23c changes to allow a easier register process 2026-07-26 23:52:08 +02:00
Aiirondev_dev 9df7a73db1 change to the CRFS 2026-07-26 23:45:49 +02:00
Aiirondev_dev dcfd23b412 changes to work 2026-07-26 23:21:56 +02:00
Aiirondev_dev d523dd0a68 change to allow for the wtf tocken to be read in correctly 2026-07-26 23:08:47 +02:00
Aiirondev_dev d7d96d5567 changes to the register Funktion 2026-07-26 22:56:39 +02:00
Aiirondev_dev a0018eebd5 slight mistake because of disregard for the new package that needs to be in the requirements 2026-07-26 22:37:45 +02:00
Aiirondev_dev eebaaef9ea changes to the register process to make it more streamlined 2026-07-26 22:30:26 +02:00
Aiirondev_dev 86112ff295 changes to the username generation to have a more brother view 2026-07-26 22:10:00 +02:00
Aiirondev_dev 048900058f Fiy for a wrong endpoint 2026-07-26 22:00:53 +02:00
Aiirondev_dev b8923b4417 Debug changes 2026-07-26 20:48:09 +02:00
Aiirondev_dev a864bab713 Fix of issues with the student user getting from the database users to student_cards 2026-07-26 20:31:16 +02:00
Aiirondev_dev 14322e11c0 ahhhhhhhh 2026-07-26 15:56:15 +02:00
Aiirondev_dev 93bb901f45 changes to make home_admmin the only home and also the main home so the style is the same and the authorisations are properly displayed 2026-07-26 15:41:31 +02:00
17 changed files with 367 additions and 535 deletions
-8
View File
@@ -1,8 +0,0 @@
services:
app:
working_dir: /app/Web
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "--timeout", "30", "--graceful-timeout", "20", "--worker-connections", "100", "--max-requests", "1000", "--max-requests-jitter", "100", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
build: null
ports:
- "10000:8000"
+1
View File
@@ -3,6 +3,7 @@ logs
certs certs
build build
.venv .venv
.idea
__pycache__ __pycache__
.pycvapid.json .pycvapid.json
Web/vapid.json Web/vapid.json
-1
View File
@@ -1 +0,0 @@
v0.8.31.1
-128
View File
@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
Iron.ai.dev@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
-55
View File
@@ -1,55 +0,0 @@
# Endbenutzer-Lizenzvertrag (EULA) und Nutzungsbedingungen
**Softwareprojekt:** Inventarsystem
**Urheberrechtshalter (Lizenzgeber):** Maximilian Gründinger
**Gültigkeit:** Stand 2026
---
### PRÄAMBEL
Dieser Endbenutzer-Lizenzvertrag (im Folgenden „Vertrag“) stellt eine rechtsgültige Vereinbarung zwischen Ihnen (im Folgenden „Lizenznehmer“) und dem Urheber **AIIrondev** (im Folgenden „Lizenzgeber“) dar. Durch den Zugriff auf den Quellcode, die Installation, das Kopieren oder die sonstige Nutzung der Software „Inventarsystem“ (im Folgenden „Produkt“) erklärt sich der Lizenznehmer mit den nachfolgenden Bedingungen vollumfänglich einverstanden.
Sollte der Lizenznehmer den Bedingungen dieses Vertrags nicht zustimmen, ist jegliche Nutzung, Vervielfältigung oder Distribution des Produkts mit sofortiger Wirkung untersagt.
---
### § 1 GEGENSTAND DER LIZENZ UND EIGENTUMSRECHTE
1. Das Produkt wird dem Lizenznehmer unter Vorbehalt lizenziert, nicht verkauft. Sämtliche Eigentumsrechte, Urheberrechte und sonstigen geistigen Eigentumsrechte am Produkt sowie an allen Kopien davon verbleiben ausschließlich beim Lizenzgeber.
2. Diese Lizenz gewährt lediglich ein eingeschränktes Nutzungsrecht unter den in diesem Vertrag explizit genannten Bedingungen.
### § 2 ZULÄSSIGER NUTZUNGSKREIS (PRIVATNUTZUNG)
1. Die unentgeltliche Nutzung des Produkts ist ausschließlich **natürlichen Personen für den rein privaten, häuslichen Gebrauch** gestattet.
2. Die Nutzung umfasst die Verwaltung privater Bestände ohne jegliche Gewinnerzielungsabsicht.
3. Jegliche Nutzung durch **Institutionelle Nutzer** (einschließlich, aber nicht beschränkt auf: Unternehmen, Einzelunternehmer, Freiberufler, Vereine, Bildungseinrichtungen, Behörden oder NGOs) ist ausdrücklich **untersagt** und bedarf einer gesonderten, schriftlichen Lizenzvereinbarung mit dem Lizenzgeber.
### § 3 FUNKTIONALE EINSCHRÄNKUNGEN UND SUPPORT
1. Dem Lizenznehmer wird das Produkt in der jeweils vorliegenden Fassung bereitgestellt („As-Is“).
2. Für die kostenlose Privatnutzung besteht **kein Anspruch** auf:
- Technischen Support oder Beratung.
- Bereitstellung von Sicherheits-Updates oder Patches.
- Gewährleistung der Kompatibilität mit spezifischen Hardware- oder Softwareumgebungen.
3. Der Lizenzgeber behält sich das Recht vor, Funktionen in zukünftigen Versionen zu ändern, einzuschränken oder kostenpflichtig zu gestalten.
### § 4 WAHRUNG DER URHEBERBEZEICHNUNG (BRANDING-KLAUSEL)
1. Das Produkt verfügt über fest integrierte Urheberrechtshinweise, insbesondere die Kennzeichnung **„Powered by AIIrondev“** in der Benutzeroberfläche (Footer/Menü).
2. Es ist dem Lizenznehmer untersagt, diese Hinweise zu entfernen, zu modifizieren, zu verdecken oder deren Sichtbarkeit durch technische Maßnahmen (z. B. Manipulation von CSS, JavaScript oder Metadaten) zu beeinträchtigen.
3. Das Entfernen dieser Hinweise führt zum sofortigen und automatischen Erlöschen der Nutzungslizenz.
### § 5 VERBOT DER KOMMERZIELLEN VERWERTUNG & SAAS
1. Die Bereitstellung des Produkts als Dienstleistung für Dritte (Software-as-a-Service, SaaS), insbesondere gegen Entgelt oder zur Generierung von Werbeeinnahmen, ist strikt untersagt.
2. Das Hosting auf öffentlichen Servern mit dem Ziel, Dritten ohne eigene Installation Zugriff auf die Funktionalität zu gewähren, ist nur mit ausdrücklicher schriftlicher Genehmigung des Lizenzgebers zulässig.
### § 6 MODIFIKATIONEN UND CONTRIBUTIONS
1. Der Lizenznehmer darf den Quellcode für den privaten Eigenbedarf modifizieren.
2. Im Falle einer Veröffentlichung von Modifikationen oder der Einreichung von Verbesserungsvorschlägen (z. B. Pull Requests auf GitHub) räumt der Lizenznehmer dem Lizenzgeber ein unwiderrufliches, weltweites, zeitlich unbeschränktes und kostenfreies Nutzungs- und Verwertungsrecht an diesen Änderungen ein. Der Lizenzgeber ist berechtigt, diese Änderungen in das Hauptprodukt zu übernehmen und unter dieser oder einer anderen Lizenz zu vertreiben.
### § 7 HAFTUNGSBESCHRÄNKUNG
1. Die Haftung des Lizenzgebers für Schäden, die aus der Nutzung oder Unmöglichkeit der Nutzung des Produkts entstehen (einschließlich Datenverlust, Betriebsunterbrechung oder entgangener Gewinn), ist auf Vorsatz und grobe Fahrlässigkeit beschränkt.
2. Der Lizenzgeber übernimmt keine Haftung für die Richtigkeit der mit dem Produkt verwalteten Daten.
### § 8 RECHTSWAHL UND GERICHTSSTAND
1. Es gilt ausschließlich das Recht der **Bundesrepublik Deutschland** unter Ausschluss des UN-Kaufrechts (CISG).
2. Soweit gesetzlich zulässig, wird als Gerichtsstand für alle Streitigkeiten aus diesem Vertrag der Sitz des Lizenzgebers vereinbart.
3. Sollten einzelne Bestimmungen dieses Vertrags unwirksam sein, bleibt die Wirksamkeit der übrigen Bestimmungen unberührt (Salvatorische Klausel).
---
**ANFRAGEN FÜR AUSNAHMEGENEHMIGUNGEN (KOMMERZIELLE LIZENZEN):** Bitte kontaktieren Sie den Urheber direkt über das GitHub-Profil: [AIIrondev auf GitHub](https://github.com/AIIrondev)
-38
View File
@@ -1,38 +0,0 @@
Release-Optionen
=================
Diese Datei beschreibt die Eingabeoptionen des CI-Workflows `.github/workflows/release-docker.yml`.
Inputs (workflow_dispatch)
- `bump` (choice)
- `patch` (Standard): Erhöht nur die Patch-Version (vX.Y.Z -> vX.Y.Z+1).
- `minor`: Erhöht die Minor-Version und setzt Patch auf 0 (vX.Y.Z -> vX.Y+1.0).
- `major`: Erhöht die Major-Version und setzt Minor/Patch auf 0 (vX.Y.Z -> vX+1.0.0).
- `development`: Erzeugt einen Development-Release mit Suffix `-dev` (z. B. `v3.1.4-dev`).
- `push_dev` (choice, optional)
- `false` (Standard): Bei `bump=development` wird das `:dev`-Image NICHT automatisch an GHCR gepusht.
- `true`: Bei `bump=development` wird zusätzlich das Image `ghcr.io/aiirondev/legendary-octo-garbanzo:dev` gepusht.
Verhalten/Anmerkungen
- Development releases werden als GitHub Release erzeugt und als `prerelease` markiert, damit sie nicht automatisch von normalen UpdateFlows genutzt werden.
- Es gibt pro Release genau einen ReleaseEintrag (für DevReleases mit `-dev` Suffix). Es wird kein separates `inventarsystem-image-dev.tar.gz` mehr erzeugt; das Update/Deployment erfolgt über den ReleaseTag / ImageTag.
- `update.sh` unterstützt weiterhin `dev`/`development`-Modus und akzeptiert nun auch explizite ReleaseTags wie `v3.1.4-dev`.
Beispiele
- Patch-Release (manuell):
- GitHub UI: Run workflow → `bump=patch`
- CLI mit `gh`:
gh workflow run release-docker.yml --repo AIIrondev/legendary-octo-garbanzo --field bump=patch
- Development prerelease (ohne Push des :dev Images):
- GitHub UI: Run workflow → `bump=development` (leave `push_dev=false`)
- Ergebnis: Release `vX.Y.Z-dev` als prerelease, Image wird nicht automatisch als `:dev` gepusht.
- Development prerelease + push des :dev Images:
- GitHub UI: Run workflow → `bump=development`, `push_dev=true`
- CLI Beispiel:
gh workflow run release-docker.yml --repo AIIrondev/legendary-octo-garbanzo --field bump=development --field push_dev=true
Empfehlung
- Verwende `bump=development` für experimentelle/early releases; Nutzer müssen explizit `./update.sh vX.Y.Z-dev` ausführen, um auf diese Version zu upgraden.
-25
View File
@@ -1,25 +0,0 @@
# Security Policy
## Supported Versions
The latest version will allways be supported the rest are old version that are not activly supported.
| Version | Supported |
| ------- | ------------------ |
| 0.2.17 | ✅ |
| 3.2.x | :white_check_mark: |
| 3.1.x | :x: |
| 3.0.x | :x: |
| 2.6.x | :x: |
| 2.4.x | :x: |
| 1.8.x | :x: |
| 1.7.x | :x: |
| 1.5.x | :x: |
| 1.4.x | :x: |
| 1.3.x | :x: |
| 1.1.x | :x: |
## Reporting a Vulnerability
To report a vulnerability contact me via. my E-Mail Iron.ai.dev@gmail.com or in insevere cases over an Issue.
+103 -120
View File
@@ -556,7 +556,7 @@ def _enforce_module_access():
flash(msg, 'info') flash(msg, 'info')
if name != 'inventory' and cfg.MODULES.is_enabled('inventory'): if name != 'inventory' and cfg.MODULES.is_enabled('inventory'):
return redirect(url_for('home')) return redirect(url_for('home_admin'))
elif name != 'library' and cfg.MODULES.is_enabled('library'): elif name != 'library' and cfg.MODULES.is_enabled('library'):
return redirect('/library') return redirect('/library')
@@ -630,7 +630,7 @@ def handle_build_error(e):
return redirect('/library') return redirect('/library')
if 'username' in session: if 'username' in session:
return redirect(url_for('home')) return redirect(url_for('home_admin'))
return redirect(url_for('login')) return redirect(url_for('login'))
@@ -708,10 +708,10 @@ def _permission_denied_fallback_endpoint(permissions, current_endpoint=None):
is_admin_user = bool(username and us.check_admin(username)) is_admin_user = bool(username and us.check_admin(username))
admin_home_allowed = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings') admin_home_allowed = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
for candidate in ('my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum', 'home'): for candidate in ('my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum', 'home_admin'):
if current_endpoint and candidate == current_endpoint: if current_endpoint and candidate == current_endpoint:
continue continue
if candidate == 'home' and is_admin_user and not admin_home_allowed: if candidate == 'home_admin' and is_admin_user and not admin_home_allowed:
continue continue
if _page_access_allowed(permissions, candidate): if _page_access_allowed(permissions, candidate):
return candidate return candidate
@@ -1984,7 +1984,7 @@ def _upload_student_cards_excel():
if not current_permissions['actions'].get('can_manage_user', False): if not current_permissions['actions'].get('can_manage_user', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
@@ -2178,7 +2178,7 @@ def _upload_excel_items(scope='inventory'):
if is_library_scope: if is_library_scope:
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
excel_file = request.files.get(file_field) excel_file = request.files.get(file_field)
if not excel_file or not excel_file.filename: if not excel_file or not excel_file.filename:
@@ -2194,7 +2194,7 @@ def _upload_excel_items(scope='inventory'):
# Allow CSV imports for authenticated non-admin users only for inventory (non-library) # Allow CSV imports for authenticated non-admin users only for inventory (non-library)
if not (is_csv and not is_library_scope and 'username' in session): if not (is_csv and not is_library_scope and 'username' in session):
flash('Einfüge-Rechte erforderlich.', 'error') flash('Einfüge-Rechte erforderlich.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
filename_lower = excel_file.filename.lower() filename_lower = excel_file.filename.lower()
if not filename_lower.endswith(('.xlsx', '.csv')): if not filename_lower.endswith(('.xlsx', '.csv')):
@@ -2882,6 +2882,7 @@ def user_status():
##################################################### changes to be made to account for the new account permison managment system ############################## ##################################################### changes to be made to account for the new account permison managment system ##############################
@app.route('/') @app.route('/')
def home(): def home():
""" """
@@ -2895,33 +2896,7 @@ def home():
flash('Bitte mit registriertem Konto anmelden!', 'error') flash('Bitte mit registriertem Konto anmelden!', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
if not cfg.MODULES.is_enabled('inventory'): return redirect(url_for('home_admin'))
if cfg.MODULES.is_enabled('library'):
return redirect('/library')
else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
elif not us.check_admin(session['username']):
return render_template(
'main.html',
username=session['username'],
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
open_item=request.args.get('open_item')
)
else:
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
if _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings'):
return redirect(url_for('home_admin'))
fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint='home')
if fallback_endpoint == 'logout':
flash('Für diesen Benutzer sind aktuell keine Seiten freigegeben.', 'error')
return redirect(url_for(fallback_endpoint))
@app.route('/home_admin') @app.route('/home_admin')
def home_admin(): def home_admin():
@@ -2942,9 +2917,12 @@ def home_admin():
else: else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403 return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
if not us.check_admin(session['username']): current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('home', False):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login')) return redirect(url_for('library_view'))
return render_template( return render_template(
'main_admin.html', 'main_admin.html',
username=session['username'], username=session['username'],
@@ -3052,7 +3030,7 @@ def library_view():
return redirect(url_for('login')) return redirect(url_for('login'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
return render_template( return render_template(
'library_table.html', 'library_table.html',
@@ -3080,7 +3058,7 @@ def library_loans_admin():
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
_ensure_audit_indexes_once() _ensure_audit_indexes_once()
@@ -3179,6 +3157,7 @@ def library_loans_admin():
'damage_text': (damage_reports[0].get('description', '') if damage_reports else ''), 'damage_text': (damage_reports[0].get('description', '') if damage_reports else ''),
'available': bool(item_doc.get('Verfuegbar', False)), 'available': bool(item_doc.get('Verfuegbar', False)),
'last_updated': fmt_dt(item_doc.get('LastUpdated')), 'last_updated': fmt_dt(item_doc.get('LastUpdated')),
'acquisition_costs': item_doc.get('Anschaffungskosten', "")
}) })
return render_template( return render_template(
@@ -3627,7 +3606,8 @@ def api_item_detail(item_id):
borrows_html = '' borrows_html = ''
if borrow_records: if borrow_records:
rows = [] rows = []
for rec in borrow_records:
for rec in borrow_records[:3]:
user_raw = rec.get('User') user_raw = rec.get('User')
try: try:
user = decrypt_text(user_raw) if user_raw is not None else '' user = decrypt_text(user_raw) if user_raw is not None else ''
@@ -3637,7 +3617,6 @@ def api_item_detail(item_id):
start = fmt_dt(rec.get('Start')) start = fmt_dt(rec.get('Start'))
end = fmt_dt(rec.get('End')) end = fmt_dt(rec.get('End'))
notes = html.escape(str(rec.get('Notes') or '')) notes = html.escape(str(rec.get('Notes') or ''))
rows.append( rows.append(
f"<li><strong>{html.escape(str(user or '-'))}</strong> — " f"<li><strong>{html.escape(str(user or '-'))}</strong> — "
f"{html.escape(str(status))}" f"{html.escape(str(status))}"
@@ -3675,7 +3654,7 @@ def api_library_item_update(item_id):
if not current_permissions['actions'].get('can_edit', False): if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403 return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
@@ -3908,7 +3887,7 @@ def student_cards_admin():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4053,7 +4032,7 @@ def student_cards_print():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4087,7 +4066,7 @@ def student_card_barcode_print():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4120,7 +4099,7 @@ def student_card_barcode_download():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4304,7 +4283,7 @@ def student_card_single_barcode_download(card_id):
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4478,7 +4457,7 @@ def login():
flask.Response: Rendered template or redirect flask.Response: Rendered template or redirect
""" """
if 'username' in session: if 'username' in session:
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if request.method == 'POST': if request.method == 'POST':
username = request.form['username'] username = request.form['username']
password = request.form['password'] password = request.form['password']
@@ -4514,7 +4493,7 @@ def login():
fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint='login') fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint='login')
return redirect(url_for(fallback_endpoint)) return redirect(url_for(fallback_endpoint))
else: else:
return redirect(url_for('home')) return redirect(url_for('home_admin'))
else: else:
app.logger.warning(f"Login failed: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}") app.logger.warning(f"Login failed: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}")
flash('Ungültige Anmeldedaten', 'error') flash('Ungültige Anmeldedaten', 'error')
@@ -4584,7 +4563,7 @@ def change_password():
# Update the password # Update the password
if us.update_password(session['username'], new_password): if us.update_password(session['username'], new_password):
flash('Ihr Passwort wurde erfolgreich geändert.', 'success') flash('Ihr Passwort wurde erfolgreich geändert.', 'success')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
else: else:
flash('Fehler beim Ändern des Passworts. Bitte versuchen Sie es später erneut.', 'error') flash('Fehler beim Ändern des Passworts. Bitte versuchen Sie es später erneut.', 'error')
@@ -5178,7 +5157,7 @@ def upload_item():
elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'): elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'):
success_redirect_endpoint = 'home_library' success_redirect_endpoint = 'home_library'
else: else:
success_redirect_endpoint = 'home' success_redirect_endpoint = 'home_admin'
# Detect if request is from mobile device # Detect if request is from mobile device
is_mobile = 'Mobile' in request.headers.get('User-Agent', '') is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
@@ -6384,11 +6363,11 @@ def delete_item(id):
if not current_permissions['actions'].get('can_delete', False): if not current_permissions['actions'].get('can_delete', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('inventory'): if not cfg.MODULES.is_enabled('inventory'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
# Resolve all related item ids (grouped variants) and load their data # Resolve all related item ids (grouped variants) and load their data
group_item_ids = it.get_group_item_ids(id) group_item_ids = it.get_group_item_ids(id)
@@ -6448,11 +6427,11 @@ def delete_library_item(id):
if not current_permissions['actions'].get('can_delete', False): if not current_permissions['actions'].get('can_delete', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
# Verify item exists and is a library item # Verify item exists and is a library item
try: try:
@@ -6522,11 +6501,11 @@ def bulk_delete_items():
if not current_permissions['actions'].get('can_delete', False): if not current_permissions['actions'].get('can_delete', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('inventory'): if not cfg.MODULES.is_enabled('inventory'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
payload = request.get_json(silent=True) or {} payload = request.get_json(silent=True) or {}
item_ids = payload.get('item_ids') or request.form.getlist('item_ids') item_ids = payload.get('item_ids') or request.form.getlist('item_ids')
@@ -6587,11 +6566,11 @@ def edit_item(id):
if not current_permissions['actions'].get('can_edit', False): if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('inventory'): if not cfg.MODULES.is_enabled('inventory'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
# Strip whitespace from all text fields # Strip whitespace from all text fields
name = sanitize_form_value(request.form.get('name')) name = sanitize_form_value(request.form.get('name'))
@@ -6721,7 +6700,7 @@ def update_group():
if not current_permissions['actions'].get('can_edit', False): if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
data = request.get_json() data = request.get_json()
series_group_id = data.get('series_group_id') series_group_id = data.get('series_group_id')
@@ -7005,7 +6984,7 @@ def ausleihen(id):
if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'): if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'):
redirect_target = 'library_view' redirect_target = 'library_view'
else: else:
redirect_target = 'home_admin' if us.check_admin(username) else 'home' # check for plausability redirect_target = 'home_admin'
item = it.get_item(id) item = it.get_item(id)
if not item: if not item:
@@ -7069,9 +7048,10 @@ def ausleihen(id):
if current_permissions['actions'].get('can_borrow') and not is_library_item: if current_permissions['actions'].get('can_borrow') and not is_library_item:
if student_card_id: if student_card_id:
student_user = us.get_user_by_student_card(student_card_id) student_user = us.get_user_by_student_card(student_card_id)
app.logger.debug(f"Borrowing on behalf of student card {student_card_id}: found user {student_user}")
if not student_user: if not student_user:
flash('Keine Schülerin/kein Schüler mit dieser Ausweis-ID gefunden.', 'error') flash('Keine Schülerin/kein Schüler mit dieser Ausweis-ID gefunden.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('library_view'))
effective_borrower = student_user.get('Username') or student_user.get('username') or username effective_borrower = student_user.get('Username') or student_user.get('username') or username
if borrow_duration_days is None: if borrow_duration_days is None:
try: try:
@@ -7321,7 +7301,7 @@ def zurueckgeben(id):
item = it.get_item(id) item = it.get_item(id)
if not item: if not item:
flash('Element nicht gefunden', 'error') flash('Element nicht gefunden', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
username = session['username'] username = session['username']
@@ -7903,20 +7883,20 @@ def terminplan():
if not cfg.MODULES.is_enabled('terminplan'): if not cfg.MODULES.is_enabled('terminplan'):
flash('Der Terminplaner ist deaktiviert.', 'info') flash('Der Terminplaner ist deaktiviert.', 'info')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
# Make sure the template exists # Make sure the template exists
template_path = os.path.join(BASE_DIR, 'templates', 'terminplan.html') template_path = os.path.join(BASE_DIR, 'templates', 'terminplan.html')
if not os.path.exists(template_path): if not os.path.exists(template_path):
print(f"Template file not found: {template_path}") print(f"Template file not found: {template_path}")
flash('Vorlage nicht gefunden. Bitte kontaktieren Sie den Administrator.', 'error') flash('Vorlage nicht gefunden. Bitte kontaktieren Sie den Administrator.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
return render_template('terminplan.html', school_periods=SCHOOL_PERIODS) return render_template('terminplan.html', school_periods=SCHOOL_PERIODS)
except Exception as e: except Exception as e:
app.logger.error(f"Error rendering terminplan: {e}") app.logger.error(f"Error rendering terminplan: {e}")
flash('Ein Fehler ist beim Anzeigen des Kalenders aufgetreten.', 'error') flash('Ein Fehler ist beim Anzeigen des Kalenders aufgetreten.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
'''-------------------------------------------------------------------------------------------------------------ADMIN ROUTES------------------------------------------------------------------------------------------------------------------''' '''-------------------------------------------------------------------------------------------------------------ADMIN ROUTES------------------------------------------------------------------------------------------------------------------'''
@@ -7924,66 +7904,69 @@ def terminplan():
@app.route('/register', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST'])
def register(): def register():
""" """
User registration route.false User registration route.
Returns: Returns:
flask.Response: Rendered template or redirect flask.Response: Rendered template or redirect
""" """
if 'username' not in session: if 'username' not in session:
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adresse zu nutzen, versuchen Sie es erneut, nachdem Sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
if 'username' in session:
if request.method == 'POST': if request.method == 'POST':
password = request.form['password'] password = request.form['password']
name = (request.form.get('name') or '').strip() name = (request.form.get('name') or '').strip()
last_name = (request.form.get('last-name') or '').strip() last_name = (request.form.get('last-name') or '').strip()
# Generate a username from the first 2 letters of first and last name. username = us.build_unique_username_from_name(name, last_name)
username = us.build_unique_username_from_name(name, last_name)
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
if not username or not password or not name or not last_name:
flash('Bitte füllen Sie alle Felder aus', 'error')
return redirect(url_for('register'))
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip() if not us.check_password_strength(password):
use_custom_permissions = request.form.get('use_custom_permissions') == 'on' flash('Passwort ist zu schwach oder entspricht nicht den Richtlinien', 'error')
return redirect(url_for('register'))
action_permissions = None
page_permissions = None
if use_custom_permissions:
action_permissions = {}
for action_key, _ in PERMISSION_ACTION_OPTIONS:
action_permissions[action_key] = request.form.get(f'action_{action_key}') == 'on'
page_permissions = {}
for endpoint_name, _ in PERMISSION_PAGE_OPTIONS:
page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on'
if not username or not password or not name or not last_name: us.add_user(
flash('Bitte füllen Sie alle Felder aus', 'error') username,
return redirect(url_for('register')) password,
if not us.check_password_strength(password): name,
flash('Passwort ist zu schwach', 'error') last_name,
return redirect(url_for('register')) is_student=False,
student_card_id=None,
action_permissions = None max_borrow_days=None,
page_permissions = None permission_preset=permission_preset,
if use_custom_permissions: action_permissions=action_permissions,
action_permissions = {} page_permissions=page_permissions,
for action_key, _ in PERMISSION_ACTION_OPTIONS:
action_permissions[action_key] = request.form.get(f'action_{action_key}') == 'on'
page_permissions = {}
for endpoint_name, _ in PERMISSION_PAGE_OPTIONS:
page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on'
us.add_user(
username,
password,
name,
last_name,
is_student=False,
student_card_id=None,
max_borrow_days=None,
permission_preset=permission_preset,
action_permissions=action_permissions,
page_permissions=page_permissions,
)
return redirect(url_for('home'))
return render_template(
'register.html',
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
) )
flash('Sie sind nicht berechtigt, diese Seite anzuzeigen', 'error')
return redirect(url_for('login')) flash(f'Benutzer "{username}" wurde erfolgreich registriert!', 'success')
return redirect(url_for('home_admin'))
return render_template(
'register.html',
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
permission_presets=getattr(us, 'PERMISSION_PRESETS', {}),
permission_action_options=PERMISSION_ACTION_OPTIONS,
permission_page_options=PERMISSION_PAGE_OPTIONS
)
@app.route('/user_del', methods=['GET']) @app.route('/user_del', methods=['GET'])
def user_del(): def user_del():
@@ -8658,7 +8641,7 @@ def admin_create_invoice(borrow_id):
'damage_reason': damage_reason, 'damage_reason': damage_reason,
'created_at': now, 'created_at': now,
'created_by': session.get('username', ''), 'created_by': session.get('username', ''),
'borrower': borrower, 'borrower': decrypt_text(borrower),
'item_id': str(item_doc['_id']), 'item_id': str(item_doc['_id']),
'item_name': item_name, 'item_name': item_name,
'item_code': item_code, 'item_code': item_code,
@@ -9108,7 +9091,7 @@ def library_item_invoices(item_id):
if not current_permissions['pages'].get('library_loans_admin', False): if not current_permissions['pages'].get('library_loans_admin', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
client = None client = None
try: try:
@@ -10015,7 +9998,7 @@ def fetch_book_info(isbn):
if not current_permissions['actions'].get('can_insert', False): if not current_permissions['actions'].get('can_insert', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403 return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
@@ -10066,7 +10049,7 @@ def download_book_cover():
if not current_permissions['actions'].get('can_insert', False): if not current_permissions['actions'].get('can_insert', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403 return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
@@ -10438,7 +10421,7 @@ def notifications_view():
except Exception as exc: except Exception as exc:
app.logger.error(f"Error loading notifications: {exc}") app.logger.error(f"Error loading notifications: {exc}")
flash('Fehler beim Laden der Benachrichtigungen.', 'error') flash('Fehler beim Laden der Benachrichtigungen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
finally: finally:
if client: if client:
client.close() client.close()
+10 -10
View File
@@ -97,18 +97,18 @@ def build_name_synonym(first_name, last_name=''):
last = _clean_name_fragment(last_name) last = _clean_name_fragment(last_name)
if first and last: if first and last:
return (first[:2] + last[:2]).title() return (first[:3] + last[:3]).title()
combined = (first + last) combined = (first + last)
if not combined: if not combined:
return 'User' return 'User'
return combined[:4].title() return combined[:6].title()
def build_username_from_name(first_name, last_name=''): 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 2 letters from each name and stores it lowercase. Uses 3 letters from each name and stores it lowercase.
Args: Args:
first_name (str): First name first_name (str): First name
@@ -123,12 +123,12 @@ def build_username_from_name(first_name, last_name=''):
def build_unique_username_from_name(first_name, last_name=''): def build_unique_username_from_name(first_name, last_name=''):
""" """
Build a unique username from the first 2 letters of the first name and Build a unique username from the first 3 letters of the first name and
the first 2 letters of the last name. the first 3 letters of the last name.
""" """
first = _clean_name_fragment(first_name) first = _clean_name_fragment(first_name)
last = _clean_name_fragment(last_name) last = _clean_name_fragment(last_name)
base_username = (first[:2] + last[:2]).lower() base_username = (first[:3] + last[:3]).lower()
if not base_username: if not base_username:
base_username = 'user' base_username = 'user'
@@ -594,8 +594,8 @@ def student_card_exists(student_card_id):
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['student_cards']
exists = users.find_one({'StudentCardId': normalized}) is not None exists = users.find_one({'SchülerName': normalized}) is not None
client.close() client.close()
return exists return exists
@@ -607,8 +607,8 @@ def get_user_by_student_card(student_card_id):
return None return 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['student_cards']
found_user = users.find_one({'StudentCardId': normalized}) found_user = users.find_one({'SchülerName': normalized})
client.close() client.close()
return found_user return found_user
+1
View File
@@ -1,4 +1,5 @@
flask flask
flask-wtf
werkzeug werkzeug
gunicorn gunicorn
pymongo pymongo
+24 -15
View File
@@ -1375,8 +1375,10 @@
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li> <li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li>
{% endif %} {% endif %}
{% if student_cards_module_enabled %} {% if student_cards_module_enabled %}
{% if current_permissions.actions.get('can_manage_users', False) %}
<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 %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.pages.get('admin_school_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 %}
@@ -1619,11 +1621,10 @@
<div id="cookie-banner" role="dialog" aria-live="polite" aria-label="Cookie-Hinweis"> <div id="cookie-banner" role="dialog" aria-live="polite" aria-label="Cookie-Hinweis">
<div class="cb-inner"> <div class="cb-inner">
<div class="cb-text"> <div class="cb-text">
Wir verwenden technisch notwendige Cookies, um Ihre Sitzung zu verwalten und die Anwendung bereitzustellen. Bitte akzeptieren Sie Cookies, um fortzufahren. Wir verwenden ausschließlich technisch notwendige Cookies, um Ihre Sitzung zu verwalten und die Anwendung bereitzustellen. Bitte bestätigen Sie dies, um fortzufahren.
</div> </div>
<div class="cb-actions"> <div class="cb-actions">
<button class="btn-decline" id="cookie-decline">Ablehnen</button> <button class="btn-accept" id="cookie-accept">Notwendige Cookies akzeptieren</button>
<button class="btn-accept" id="cookie-accept">Akzeptieren</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1728,31 +1729,39 @@
(function(){ (function(){
function getCookie(name){ function getCookie(name){
const v = document.cookie.split(';').map(s=>s.trim()); const v = document.cookie.split(';').map(s=>s.trim());
for(const c of v){ if(c.startsWith(name+'=')) return decodeURIComponent(c.split('=')[1]); } for(const c of v){
if(c.startsWith(name+'=')) return decodeURIComponent(c.split('=')[1]);
}
return null; return null;
} }
function setCookie(name, value, days){ function setCookie(name, value, days){
const d = new Date(); d.setTime(d.getTime() + (days*24*60*60*1000)); const d = new Date();
d.setTime(d.getTime() + (days*24*60*60*1000));
document.cookie = name + '=' + encodeURIComponent(value) + ';expires=' + d.toUTCString() + ';path=/;SameSite=Lax'; document.cookie = name + '=' + encodeURIComponent(value) + ';expires=' + d.toUTCString() + ';path=/;SameSite=Lax';
} }
function showBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'block'; }
function hideBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'none'; }
// If not decided yet, show banner and block app until decision function showBanner(){
var el = document.getElementById('cookie-banner');
if(el) el.style.display = 'block';
}
function hideBanner(){
var el = document.getElementById('cookie-banner');
if(el) el.style.display = 'none';
}
// Prüfen, ob der Nutzer bereits zugestimmt hat
const consent = getCookie('cookie_consent'); const consent = getCookie('cookie_consent');
if(!consent){ if(!consent){
showBanner(); showBanner();
// Optionally blur content until consent
document.body.style.filter = 'none';
} }
// Nur noch der Akzeptieren-Button für vitale Cookies ist vorhanden
document.getElementById('cookie-accept')?.addEventListener('click', function(){ document.getElementById('cookie-accept')?.addEventListener('click', function(){
setCookie('cookie_consent','accepted',365); setCookie('cookie_consent', 'vital_accepted', 365);
hideBanner(); hideBanner();
}); });
document.getElementById('cookie-decline')?.addEventListener('click', function(){
setCookie('cookie_consent','declined',365);
window.location.href = 'https://www.ecosia.org/';
});
const username = {{ (session['username'] if 'username' in session else '')|tojson }}; const username = {{ (session['username'] if 'username' in session else '')|tojson }};
const isTutorialPage = window.location.pathname === {{ url_for('tutorial_page')|tojson }}; const isTutorialPage = window.location.pathname === {{ url_for('tutorial_page')|tojson }};
+30 -15
View File
@@ -520,7 +520,16 @@
window.openDamageReportPrompt = openDamageReportPrompt; window.openDamageReportPrompt = openDamageReportPrompt;
function openDamageInvoiceModal(row, description) { function openDamageInvoiceModal(row, description) {
if (!damageInvoiceModal || !damageInvoiceForm) { const modal = document.getElementById('damage-invoice-modal');
const form = document.getElementById('damage-invoice-form');
const inputItem = document.getElementById('damage-invoice-item');
const inputBorrower = document.getElementById('damage-invoice-borrower');
const inputCode = document.getElementById('damage-invoice-code');
const inputAmount = document.getElementById('damage-invoice-amount');
const inputReason = document.getElementById('damage-invoice-reason');
if (!modal || !form) {
console.error("Modal oder Formular nicht gefunden.");
return; return;
} }
@@ -530,23 +539,29 @@
const itemCode = row.dataset.itemCode || ''; const itemCode = row.dataset.itemCode || '';
const itemCost = row.dataset.itemCost || ''; const itemCost = row.dataset.itemCost || '';
damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId); form.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
damageInvoiceItem.value = itemName;
damageInvoiceBorrower.value = borrower; inputItem.value = itemName;
damageInvoiceCode.value = itemCode; inputBorrower.value = borrower;
damageInvoiceAmount.value = String(itemCost).replace(' EUR', '').trim(); inputCode.value = itemCode;
damageInvoiceReason.value = description || `Schaden gemeldet für ${itemName}`;
damageInvoiceModal.style.display = 'block'; inputAmount.value = String(itemCost).replace(' EUR', '').trim();
damageInvoiceAmount.focus();
inputReason.value = description || `Schaden gemeldet für ${itemName}`;
modal.style.display = 'block';
inputAmount.focus();
}
function closeDamageInvoiceModal() {
const modal = document.getElementById('damage-invoice-modal');
if (modal) {
modal.style.display = 'none';
}
} }
window.openDamageInvoiceModal = openDamageInvoiceModal; window.openDamageInvoiceModal = openDamageInvoiceModal;
window.closeDamageInvoiceModal = closeDamageInvoiceModal;
function closeDamageInvoiceModal() {
if (damageInvoiceModal) {
damageInvoiceModal.style.display = 'none';
}
}
if (damageInvoiceModal) { if (damageInvoiceModal) {
damageInvoiceModal.addEventListener('click', function(event) { damageInvoiceModal.addEventListener('click', function(event) {
+2 -2
View File
@@ -1416,8 +1416,8 @@
<div> <div>
<label for="editLibraryType">Medientyp</label> <label for="editLibraryType">Medientyp</label>
<select id="editLibraryType" style="width: 100%;"> <select id="editLibraryType" style="width: 100%;">
<option value="book">Buch</option> <option value="Buch">Buch</option>
<option value="schoolbook">Schulbuch</option> <option value="Schulbuch">Schulbuch</option>
<option value="cd">CD</option> <option value="cd">CD</option>
<option value="dvd">DVD</option> <option value="dvd">DVD</option>
<option value="other">Sonstige Medien</option> <option value="other">Sonstige Medien</option>
+59 -35
View File
@@ -2309,6 +2309,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</script> </script>
<div class="admin-content-container"> <div class="admin-content-container">
{% if current_permissions.actions.get('can_edit', False) %}
<!-- Admin conflict warning banner (populated by JS) --> <!-- Admin conflict warning banner (populated by JS) -->
<div id="conflict-banner" style="display:none; background:#fff3cd; border:1px solid #ffc107; border-left:4px solid #fd7e14; color:#856404; padding:12px 16px; border-radius:4px; margin-bottom:16px; position:relative;"> <div id="conflict-banner" style="display:none; background:#fff3cd; border:1px solid #ffc107; border-left:4px solid #fd7e14; color:#856404; padding:12px 16px; border-radius:4px; margin-bottom:16px; position:relative;">
<strong>⚠ Buchungskonflikte erkannt:</strong> <strong>⚠ Buchungskonflikte erkannt:</strong>
@@ -2335,6 +2336,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
.catch(() => {}); .catch(() => {});
}); });
</script> </script>
{% endif %}
<div class="content"> <div class="content">
<h1 style="position:relative;">Inventar Objekte <h1 style="position:relative;">Inventar Objekte
<div class="view-switch-group"> <div class="view-switch-group">
@@ -2344,14 +2347,17 @@ document.addEventListener('DOMContentLoaded', ()=>{
<button id="toggle-favorites-view" class="view-toggle-btn" title="Nur Merkliste anzeigen"> <button id="toggle-favorites-view" class="view-toggle-btn" title="Nur Merkliste anzeigen">
<span id="favorites-view-icon">🔖</span> <span id="favorites-view-icon">🔖</span>
</button> </button>
{% if current_permissions.actions.get('can_delete', False) %}
<div class="bulk-delete-inline-wrap"> <div class="bulk-delete-inline-wrap">
<button type="button" id="bulk-delete-drawer-toggle" class="view-toggle-btn bulk-delete-drawer-toggle" aria-expanded="false" title="Massenlöschung" onclick="toggleBulkDeleteDrawer()"> <button type="button" id="bulk-delete-drawer-toggle" class="view-toggle-btn bulk-delete-drawer-toggle" aria-expanded="false" title="Massenlöschung" onclick="toggleBulkDeleteDrawer()">
<span class="drawer-icon"></span> <span class="drawer-icon"></span>
</button> </button>
</div> </div>
{% endif %}
</div> </div>
</h1> </h1>
<div id="items-indicator" class="items-indicator">Objekte im System: 0</div> <div id="items-indicator" class="items-indicator">Objekte im System: 0</div>
{% if current_permissions.actions.get('can_delete', False) %}
<div id="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true"> <div id="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true">
<div class="bulk-delete-content"> <div class="bulk-delete-content">
<strong>Massenlöschung nach Kriterien</strong> <strong>Massenlöschung nach Kriterien</strong>
@@ -2364,6 +2370,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<button type="button" id="bulk-delete-button" class="danger" onclick="deleteSelectedItems()" disabled>Auswahl löschen</button> <button type="button" id="bulk-delete-button" class="danger" onclick="deleteSelectedItems()" disabled>Auswahl löschen</button>
</div> </div>
</div> </div>
{% endif %}
<div class="filter-container"> <div class="filter-container">
<div class="filter-group"> <div class="filter-group">
<div class="filter-header"> <div class="filter-header">
@@ -2469,6 +2476,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
<!-- Add the edit modal form --> <!-- Add the edit modal form -->
{% if current_permissions.actions.get('can_edit', False) %}
<div id="edit-modal" class="item-modal"> <div id="edit-modal" class="item-modal">
<div class="modal-content"> <div class="modal-content">
<span class="close-modal" onclick="closeEditModal()">&times;</span> <span class="close-modal" onclick="closeEditModal()">&times;</span>
@@ -2646,6 +2654,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</form> </form>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<!-- Schedule Appointment Modal --> <!-- Schedule Appointment Modal -->
@@ -2724,7 +2733,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
</div> </div>
<!-- Initialize template variables before any JavaScript code -->
<script> <script>
// Create a global object to hold server-side template values // Create a global object to hold server-side template values
window.serverVars = {}; window.serverVars = {};
@@ -3239,7 +3247,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
return; return;
} }
fetch('{{ url_for('bulk_delete_items') }}', { fetch("{{ url_for('bulk_delete_items') }}", {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -3703,6 +3711,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
Für Löschung markieren Für Löschung markieren
</label> </label>
</div> </div>
{% if current_permissions.actions.get('can_borrow', False) %}
${isAvailableForBorrow && !item.BlockedNow ? ${isAvailableForBorrow && !item.BlockedNow ?
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}"> `<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
${isGroupedItem ? ` ${isGroupedItem ? `
@@ -3724,12 +3733,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
: :
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>` `<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
} }
{% endif %}
{% if current_permissions.actions.get('can_delete', False) %}
<form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')"> <form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')">
<button class="delete-button" type="submit">Löschen</button> <button class="delete-button" type="submit">Löschen</button>
</form> </form>
{% endif %}
{% if current_permissions.actions.get('can_edit', False) %}
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button> <button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button>
{% endif %}
{% if current_permissions.actions.get('can_insert', False) %}
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button> <button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''} ${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
{% endif %}
</div> </div>
`; `;
itemsContainer.appendChild(card); itemsContainer.appendChild(card);
@@ -4506,10 +4524,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
: '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>'; : '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>';
modalContent.innerHTML = ` modalContent.innerHTML = `
<h2>${item.Name}</h2> <h2>${escapeHtml(item.Name || '')}</h2>
${borrowerInfoHtml} ${borrowerInfoHtml}
${appointmentInfoHtml} ${appointmentInfoHtml}
<div class="modal-image-container"> <div class="modal-image-container">
${imagesHtml} ${imagesHtml}
${item.Images && item.Images.length > 1 ? ` ${item.Images && item.Images.length > 1 ? `
@@ -4518,66 +4536,67 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="image-counter">1/${item.Images.length}</div> <div class="image-counter">1/${item.Images.length}</div>
` : ''} ` : ''}
</div> </div>
<div class="modal-details"> <div class="modal-details">
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Ort:</div> <div class="detail-label">Ort:</div>
<div class="detail-value">${item.Ort || '-'}</div> <div class="detail-value">${escapeHtml(item.Ort || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Status:</div> <div class="detail-label">Status:</div>
<div class="detail-value ${isBorrowed ? 'status-borrowed' : ''}"> <div class="detail-value ${isBorrowed ? 'status-borrowed' : ''}">
${isBorrowed ? 'Ausgeliehen' : 'Verfügbar'} ${isBorrowed ? 'Ausgeliehen' : 'Verfügbar'}
</div> </div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Unterrichtsfach:</div> <div class="detail-label">Unterrichtsfach:</div>
<div class="detail-value">${filter1Array.join(', ') || '-'}</div> <div class="detail-value">${escapeHtml(filter1Array.join(', ') || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Jahrgangsstufe:</div> <div class="detail-label">Jahrgangsstufe:</div>
<div class="detail-value">${filter2Array.join(', ') || '-'}</div> <div class="detail-value">${escapeHtml(filter2Array.join(', ') || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Schlagwort:</div> <div class="detail-label">Schlagwort:</div>
<div class="detail-value">${filter3Array.join(', ') || '-'}</div> <div class="detail-value">${escapeHtml(filter3Array.join(', ') || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Code:</div> <div class="detail-label">Code:</div>
<div class="detail-value">${item.Code_4 || '-'}</div> <div class="detail-value">${escapeHtml(item.Code_4 || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Anzahl:</div> <div class="detail-label">Anzahl:</div>
<div class="detail-value">${item.GroupedDisplayCount || 1}</div> <div class="detail-value">${escapeHtml(String(item.GroupedDisplayCount || 1))}</div>
</div> </div>
${isGroupedItem ? ` ${isGroupedItem ? `
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Verfügbar:</div> <div class="detail-label">Verfügbar:</div>
<div class="detail-value">${availableGroupedCount}</div> <div class="detail-value">${escapeHtml(String(availableGroupedCount))}</div>
</div>` : ''} </div>` : ''}
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Anschaffungsjahr:</div> <div class="detail-label">Anschaffungsjahr:</div>
<div class="detail-value">${item.Anschaffungsjahr || '-'}</div> <div class="detail-value">${escapeHtml(String(item.Anschaffungsjahr || '-'))}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anschaffungskosten:</div>
<div class="detail-value">${item.Anschaffungskosten ? item.Anschaffungskosten + ' €' : '-'}</div>
</div>
<div class="detail-group full-width">
<div class="detail-label">Beschreibung:</div>
<div class="detail-value">${item.Beschreibung || '-'}</div>
</div> </div>
<div class="detail-group">
<div class="detail-label">Anschaffungskosten:</div>
<div class="detail-value">${item.Anschaffungskosten ? escapeHtml(String(item.Anschaffungskosten)) + ' €' : '-'}</div>
</div>
<div class="detail-group full-width">
<div class="detail-label">Beschreibung:</div>
<div class="detail-value">${escapeHtml(item.Beschreibung || '-')}</div>
</div>
{% if current_permissions.actions.get('can_view_logs', False) %}
<div class="detail-group full-width" style="margin-top:12px;"> <div class="detail-group full-width" style="margin-top:12px;">
<div class="detail-label" style="font-weight:600; color:#374151;">Beschädigungs-Historie</div> <div class="detail-label" style="font-weight:600; color:#374151;">Beschädigungs-Historie</div>
<div class="detail-value"> <div class="detail-value">
@@ -4590,7 +4609,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<div class="detail-group full-width" style="margin-top:12px; padding:10px; border:1px solid #e3e3e3; border-radius:8px;"> <div class="detail-group full-width" style="margin-top:12px; padding:10px; border:1px solid #e3e3e3; border-radius:8px;">
<div class="detail-label">Verfügbarkeit prüfen:</div> <div class="detail-label">Verfügbarkeit prüfen:</div>
<div class="detail-value"> <div class="detail-value">
@@ -4611,7 +4631,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
</div> </div>
</div> </div>
<div class="detail-group full-width" style="margin-top:15px;"> <div class="detail-group full-width" style="margin-top:15px;">
<div class="detail-label">Geplante Ausleihen:</div> <div class="detail-label">Geplante Ausleihen:</div>
<div class="detail-value"> <div class="detail-value">
@@ -4635,9 +4655,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
</div> </div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
${!isBorrowed ? ${!isBorrowed ?
{% if current_permissions.actions.get('can_borrow', False) %}
`<form method="POST" action="/ausleihen/${item._id}"> `<form method="POST" action="/ausleihen/${item._id}">
${isGroupedItem ? ` ${isGroupedItem ? `
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;"> <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
@@ -4651,6 +4672,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>` : ''} </div>` : ''}
<button class="ausleihen" type="submit">Ausleihen</button> <button class="ausleihen" type="submit">Ausleihen</button>
</form>` </form>`
{% else %}
`<button class="ausleihen disabled-button" disabled title="Keine Berechtigung">Ausleihen nicht möglich</button>`
{% endif %}
: (!isGroupedItem && item.User === currentUsername) ? : (!isGroupedItem && item.User === currentUsername) ?
`<form method="POST" action="/zurueckgeben/${item._id}"> `<form method="POST" action="/zurueckgeben/${item._id}">
<button class="ausleihen" type="submit">Zurückgeben</button> <button class="ausleihen" type="submit">Zurückgeben</button>
@@ -4659,7 +4683,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
} }
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button> <button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button>
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button> <button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
${damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`} ${damageReports && damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''} ${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')"> <form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
<button class="delete-button" type="submit">Löschen</button> <button class="delete-button" type="submit">Löschen</button>
+133 -83
View File
@@ -1,11 +1,3 @@
<!--
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
See Legal/LICENSE for the full license text.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev
-->
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Register{% endblock %} {% block title %}Register{% endblock %}
@@ -33,6 +25,7 @@
<div class="content"> <div class="content">
<div class="form-card"> <div class="form-card">
<form method="POST" action="{{ url_for('register') }}"> <form method="POST" action="{{ url_for('register') }}">
<div class="form-group"> <div class="form-group">
<label for="name">Vorname</label> <label for="name">Vorname</label>
<div class="input-container"> <div class="input-container">
@@ -44,7 +37,7 @@
<span class="input-icon">👤</span> <span class="input-icon">👤</span>
<input type="text" id="last-name" name="last-name" placeholder="Geben Sie den Nachnamen ein" required onchange="generateUsername()" oninput="generateUsername()"> <input type="text" id="last-name" name="last-name" placeholder="Geben Sie den Nachnamen ein" required onchange="generateUsername()" oninput="generateUsername()">
</div> </div>
<label for="username">Benutzername <span style="color: #9ca3af;">(wird automatisch generiert)</span></label> <label for="username">Benutzername <span style="color: #9ca3af;">(Vorschau - wird serverseitig finalisiert)</span></label>
<div class="input-container"> <div class="input-container">
<span class="input-icon">👤</span> <span class="input-icon">👤</span>
<input type="text" id="username" name="username" placeholder="Automatisch aus Name und Nachname" readonly style="background-color: #f3f4f6; cursor: not-allowed;"> <input type="text" id="username" name="username" placeholder="Automatisch aus Name und Nachname" readonly style="background-color: #f3f4f6; cursor: not-allowed;">
@@ -64,9 +57,21 @@
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li> <li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
</ul> </ul>
</div> </div>
<div class="input-container">
<span class="input-icon">🔒</span> <div class="input-wrapper">
<input type="password" id="password" name="password" placeholder="Geben Sie ein sicheres Passwort ein" required> <div class="input-container">
<span class="input-icon">🔒</span>
<!-- HTML5 Pattern blockiert unsichere Passwörter vor dem Absenden -->
<input
id="password"
name="password"
placeholder="Geben Sie ein sicheres Passwort ein"
required
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{12,}">
</div>
</div>
<div class="pw-actions">
<button type="button" class="btn-secondary" onclick="generateSecurePassword()">Passwort generieren</button>
</div> </div>
</div> </div>
@@ -258,31 +263,6 @@ input::placeholder {
background-color: #27ae60; background-color: #27ae60;
} }
.navigation-buttons {
text-align: center;
margin: 1.5rem 0;
}
.back-button {
display: inline-flex;
align-items: center;
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
transition: var(--transition);
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
}
.back-button:hover {
background-color: rgba(52, 152, 219, 0.1);
}
.back-icon {
margin-right: 0.5rem;
font-size: 1.2rem;
}
.flash-container { .flash-container {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@@ -422,49 +402,147 @@ input::placeholder {
margin: 4px 0; margin: 4px 0;
color: #1f2937; color: #1f2937;
} }
/* Neue Styles für die Passwort-Erweiterungen */
.pw-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.btn-secondary {
padding: 8px 12px;
border: 1px solid #d1d5db;
background-color: #f3f4f6;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: #e5e7eb;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.input-wrapper .input-container {
flex-grow: 1;
margin: 0;
}
</style> </style>
<script> <script>
// Function to generate username from first and last name (helper function) // Hilfsfunktion: Umlaute auflösen und Sonderzeichen entfernen
function cleanNameForUsername(text) { function cleanNameForUsername(text) {
if (!text) return ''; if (!text) return '';
// Remove special characters, convert umlauts, lowercase let cleaned = text.trim().toLowerCase()
let cleaned = text
.replace(/[^a-zA-Zäöüß\s-]/g, '')
.trim()
.toLowerCase();
// Convert German umlauts to ASCII
cleaned = cleaned
.replace(/ä/g, 'ae') .replace(/ä/g, 'ae')
.replace(/ö/g, 'oe') .replace(/ö/g, 'oe')
.replace(/ü/g, 'ue') .replace(/ü/g, 'ue')
.replace(/ß/g, 'ss'); .replace(/ß/g, 'ss')
.replace(/[^a-z]/g, '');
return cleaned; return cleaned;
} }
// Generate username from name and last_name fields // Hilfsfunktion: Erster Buchstabe groß
function formatPart(str, len) {
if (!str) return '';
const part = str.slice(0, len);
return part.charAt(0).toUpperCase() + part.slice(1);
}
// Generiert die Vorschau des Benutzernamens (z.B. SimFri)
function generateUsername() { function generateUsername() {
const firstName = cleanNameForUsername(document.getElementById('name').value || ''); const firstName = cleanNameForUsername(document.getElementById('name').value);
const lastName = cleanNameForUsername(document.getElementById('last-name').value || ''); const lastName = cleanNameForUsername(document.getElementById('last-name').value);
let username = ''; let username = '';
if (firstName && lastName) { if (firstName && lastName) {
username = (firstName.slice(0, 3) + lastName.slice(0, 3)); username = formatPart(firstName, 3) + formatPart(lastName, 3);
} else if (firstName) { } else if (firstName) {
username = firstName.slice(0, 6); username = formatPart(firstName, 6);
} else if (lastName) { } else if (lastName) {
username = lastName.slice(0, 6); username = formatPart(lastName, 6);
} }
// Set the username field
const usernameField = document.getElementById('username'); const usernameField = document.getElementById('username');
if (usernameField) { if (usernameField) {
usernameField.value = username || ''; usernameField.value = username || '';
} }
} }
// PASSWORT GENERATOR
function generateSecurePassword() {
const length = 16;
const charsetLower = "abcdefghijklmnopqrstuvwxyz";
const charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const charsetNum = "0123456789";
const charsetSym = "!@#$%^&*()_+~|}{[]:;?><,.-=";
let password = "";
// Garantiert mindestens 1 Zeichen aus jeder Kategorie
password += charsetLower[Math.floor(Math.random() * charsetLower.length)];
password += charsetUpper[Math.floor(Math.random() * charsetUpper.length)];
password += charsetNum[Math.floor(Math.random() * charsetNum.length)];
password += charsetSym[Math.floor(Math.random() * charsetSym.length)];
const allChars = charsetLower + charsetUpper + charsetNum + charsetSym;
// Restliche Zeichen auffüllen
for (let i = 4; i < length; i++) {
password += allChars[Math.floor(Math.random() * allChars.length)];
}
// Passwort durchmischen
password = password.split('').sort(() => 0.5 - Math.random()).join('');
const pwField = document.getElementById('password');
pwField.value = password;
// Automatisch sichtbar machen, damit der User es kopieren kann
pwField.type = 'text';
document.getElementById('toggle-pw-btn').textContent = '🙈';
updatePasswordRules();
}
// PASSWORT SICHTBARKEIT UMSCHALTEN
function togglePasswordVisibility() {
const pwField = document.getElementById('password');
const toggleBtn = document.getElementById('toggle-pw-btn');
if (pwField.type === "password") {
pwField.type = "text";
toggleBtn.textContent = '🙈';
} else {
pwField.type = "password";
toggleBtn.textContent = '👁️';
}
}
// Globale Funktion für Passwort-Update
function updatePasswordRules() {
const passwordInput = document.getElementById('password');
if (!passwordInput) return;
const value = String(passwordInput.value || '');
const setRuleState = (id, ok) => {
const node = document.getElementById(id);
if (node) node.classList.toggle('ok', !!ok);
};
setRuleState('pw-rule-length', value.length >= 12);
setRuleState('pw-rule-lower', /[a-z]/.test(value));
setRuleState('pw-rule-upper', /[A-Z]/.test(value));
setRuleState('pw-rule-digit', /[0-9]/.test(value));
setRuleState('pw-rule-symbol', /[^A-Za-z0-9]/.test(value));
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const permissionPresets = {{ permission_presets | tojson }}; const permissionPresets = {{ permission_presets | tojson }};
const presetSelect = document.getElementById('permission-preset'); const presetSelect = document.getElementById('permission-preset');
@@ -507,34 +585,6 @@ document.addEventListener('DOMContentLoaded', function () {
} }
const passwordInput = document.getElementById('password'); const passwordInput = document.getElementById('password');
const passwordRules = {
length: document.getElementById('pw-rule-length'),
lower: document.getElementById('pw-rule-lower'),
upper: document.getElementById('pw-rule-upper'),
digit: document.getElementById('pw-rule-digit'),
symbol: document.getElementById('pw-rule-symbol')
};
function setRuleState(node, ok) {
if (!node) {
return;
}
node.classList.toggle('ok', !!ok);
}
function updatePasswordRules() {
if (!passwordInput) {
return;
}
const value = String(passwordInput.value || '');
setRuleState(passwordRules.length, value.length >= 12);
setRuleState(passwordRules.lower, /[a-z]/.test(value));
setRuleState(passwordRules.upper, /[A-Z]/.test(value));
setRuleState(passwordRules.digit, /[0-9]/.test(value));
setRuleState(passwordRules.symbol, /[^A-Za-z0-9]/.test(value));
}
if (passwordInput) { if (passwordInput) {
passwordInput.addEventListener('input', updatePasswordRules); passwordInput.addEventListener('input', updatePasswordRules);
passwordInput.addEventListener('blur', updatePasswordRules); passwordInput.addEventListener('blur', updatePasswordRules);
+1
View File
@@ -1,4 +1,5 @@
flask flask
flask-wtf
werkzeug werkzeug
gunicorn gunicorn
pymongo==4.6.3 pymongo==4.6.3
+3
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
sudo find /tmp -maxdepth 1 -name "tmp.*" -exec rm -rf {} +
echo "Cleaning up old temporary files in /tmp..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"