Compare commits

...

15 Commits

Author SHA1 Message Date
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
6 changed files with 216 additions and 152 deletions
+55 -52
View File
@@ -3605,7 +3605,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 ''
@@ -3615,7 +3616,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))}"
@@ -7903,66 +7903,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 3 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_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
) )
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():
+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 }};
+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>
+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