Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd6915a923 | |||
| 9b7ba39702 | |||
| 3b637de188 | |||
| c0f49ab8de |
+81
-32
@@ -208,8 +208,8 @@ PERMISSION_ACTION_ENDPOINTS = {
|
||||
'admin_update_user_permissions': 'can_manage_users',
|
||||
'admin_anonymize_names': 'can_manage_users',
|
||||
'home_admin': 'can_manage_settings',
|
||||
'upload_admin': 'can_manage_settings',
|
||||
'library_admin': 'can_manage_settings',
|
||||
'upload_admin': 'can_insert',
|
||||
'library_admin': 'can_insert',
|
||||
'admin_borrowings': 'can_manage_settings',
|
||||
'library_loans_admin': 'can_manage_settings',
|
||||
'admin_damaged_items': 'can_manage_settings',
|
||||
@@ -1826,8 +1826,9 @@ def _upload_excel_items(scope='inventory'):
|
||||
flash('Nicht angemeldet.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if not us.check_admin(session['username']):
|
||||
flash('Administratorrechte erforderlich.', 'error')
|
||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||
if not _action_access_allowed(permissions, 'can_insert'):
|
||||
flash('Einfüge-Rechte erforderlich.', 'error')
|
||||
return redirect(url_for('home'))
|
||||
|
||||
is_library_scope = scope == 'library'
|
||||
@@ -1837,7 +1838,7 @@ def _upload_excel_items(scope='inventory'):
|
||||
if is_library_scope:
|
||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
||||
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for('home'))
|
||||
|
||||
excel_file = request.files.get(file_field)
|
||||
if not excel_file or not excel_file.filename:
|
||||
@@ -3017,8 +3018,8 @@ def api_library_item_update(item_id):
|
||||
@app.route('/upload_admin')
|
||||
def upload_admin():
|
||||
"""
|
||||
Admin upload page route.
|
||||
Only accessible by users with admin privileges.
|
||||
Upload page route for inventory items.
|
||||
Accessible to users with insert permission.
|
||||
Supports duplication by passing duplicate_from parameter.
|
||||
|
||||
Returns:
|
||||
@@ -3027,7 +3028,8 @@ def upload_admin():
|
||||
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')
|
||||
return redirect(url_for('login'))
|
||||
if not us.check_admin(session['username']):
|
||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||
if not _action_access_allowed(permissions, 'can_insert'):
|
||||
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'))
|
||||
|
||||
@@ -3091,13 +3093,14 @@ def upload_admin():
|
||||
@app.route('/library_admin')
|
||||
def library_admin():
|
||||
"""
|
||||
Dedicated admin page for library/book uploads with ISBN scanning.
|
||||
Only accessible by admins and only when the library module is enabled.
|
||||
Dedicated page for library/book uploads with ISBN scanning.
|
||||
Accessible to users with insert permission when the library module is enabled.
|
||||
"""
|
||||
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')
|
||||
return redirect(url_for('login'))
|
||||
if not us.check_admin(session['username']):
|
||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||
if not _action_access_allowed(permissions, 'can_insert'):
|
||||
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'))
|
||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
||||
@@ -3730,6 +3733,13 @@ def login():
|
||||
is_admin_user = bool(user.get('Admin', False))
|
||||
session['admin'] = is_admin_user
|
||||
session['is_admin'] = is_admin_user
|
||||
# Bind session favorites to the authenticated user to avoid cross-user leakage.
|
||||
try:
|
||||
session['favorites_owner'] = username
|
||||
session['favorites'] = list(dict.fromkeys([str(f) for f in us.get_favorites(username)]))
|
||||
except Exception:
|
||||
session['favorites_owner'] = username
|
||||
session['favorites'] = []
|
||||
if is_admin_user:
|
||||
permissions = us.get_effective_permissions(username)
|
||||
if _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings'):
|
||||
@@ -3824,6 +3834,8 @@ def logout():
|
||||
session.pop('username', None)
|
||||
session.pop('admin', None)
|
||||
session.pop('is_admin', None)
|
||||
session.pop('favorites', None)
|
||||
session.pop('favorites_owner', None)
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
@@ -3832,6 +3844,7 @@ def get_items():
|
||||
"""Return items plus merged favorites (session + DB) and per-item favorite flag."""
|
||||
client = None
|
||||
try:
|
||||
_ensure_session_favs()
|
||||
username = session.get('username')
|
||||
# Merge DB favorites into session if logged in
|
||||
if username:
|
||||
@@ -4078,8 +4091,23 @@ def api_booking_conflicts():
|
||||
|
||||
"""Favorites management endpoints (persistent + session cache)."""
|
||||
def _ensure_session_favs():
|
||||
if 'favorites' not in session:
|
||||
username = session.get('username')
|
||||
owner = session.get('favorites_owner')
|
||||
|
||||
if not username:
|
||||
if 'favorites' not in session or not isinstance(session.get('favorites'), list):
|
||||
session['favorites'] = []
|
||||
return
|
||||
|
||||
if owner != username:
|
||||
session['favorites_owner'] = username
|
||||
session['favorites'] = []
|
||||
session.modified = True
|
||||
return
|
||||
|
||||
if 'favorites' not in session or not isinstance(session.get('favorites'), list):
|
||||
session['favorites'] = []
|
||||
session.modified = True
|
||||
|
||||
@app.route('/favorites', methods=['GET'])
|
||||
def list_favorites():
|
||||
@@ -4166,6 +4194,7 @@ def toggle_fav(item_id):
|
||||
@app.route('/debug/favorites')
|
||||
def debug_favorites():
|
||||
"""Diagnostic endpoint: shows session favorites, DB favorites and merged output."""
|
||||
_ensure_session_favs()
|
||||
username = session.get('username')
|
||||
session_favs = list(session.get('favorites', []))
|
||||
db_favs = []
|
||||
@@ -4192,11 +4221,15 @@ def upload_item():
|
||||
if 'username' not in session:
|
||||
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
|
||||
|
||||
# Check if user is an admin
|
||||
# Check if user may insert items
|
||||
username = session['username']
|
||||
if not us.check_admin(username):
|
||||
return jsonify({'success': False, 'message': 'Administratorrechte erforderlich'}), 403
|
||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||
if not _action_access_allowed(permissions, 'can_insert'):
|
||||
return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403
|
||||
|
||||
can_access_admin_home = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
|
||||
success_redirect_endpoint = 'home_admin' if can_access_admin_home else 'home'
|
||||
|
||||
# Detect if request is from mobile device
|
||||
is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
|
||||
is_ios = 'iPhone' in request.headers.get('User-Agent', '') or 'iPad' in request.headers.get('User-Agent', '')
|
||||
@@ -4277,7 +4310,7 @@ def upload_item():
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
else:
|
||||
flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
# Expand special "all values" selections for predefined filters.
|
||||
filter_upload = expand_filter_selection(filter_upload, 1)
|
||||
@@ -4290,7 +4323,7 @@ def upload_item():
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
else:
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
item_isbn = ''
|
||||
item_type = 'general'
|
||||
@@ -4301,7 +4334,7 @@ def upload_item():
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
if item_isbn:
|
||||
item_type = 'book'
|
||||
|
||||
@@ -4311,7 +4344,7 @@ def upload_item():
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
if not item_isbn:
|
||||
error_msg = 'Für Bücher ist eine gültige ISBN erforderlich.'
|
||||
if is_mobile:
|
||||
@@ -4328,7 +4361,7 @@ def upload_item():
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
else:
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
# Check if base code is unique for single-item uploads
|
||||
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
|
||||
@@ -4337,7 +4370,7 @@ def upload_item():
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
else:
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
# Validate optional per-item codes
|
||||
if individual_codes:
|
||||
@@ -4346,14 +4379,14 @@ def upload_item():
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
if len(set(individual_codes)) != len(individual_codes):
|
||||
error_msg = 'Doppelte Einzelcodes erkannt. Bitte alle Codes eindeutig eintragen.'
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
for specific_code in individual_codes:
|
||||
if not it.is_code_unique(specific_code):
|
||||
@@ -4361,7 +4394,7 @@ def upload_item():
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
def generate_unique_batch_code(base_code, position):
|
||||
"""Generate a unique code for every item in a batch."""
|
||||
@@ -5054,7 +5087,7 @@ def upload_item():
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 400
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
parent_item_id = str(created_item_ids[0]) if created_item_ids else None
|
||||
item_id = it.add_item(
|
||||
@@ -5081,7 +5114,7 @@ def upload_item():
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 500
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
item_id = created_item_ids[0] if created_item_ids else None
|
||||
|
||||
@@ -5105,14 +5138,14 @@ def upload_item():
|
||||
})
|
||||
else:
|
||||
flash(success_msg, 'success')
|
||||
return redirect(url_for('home_admin', highlight_item=str(item_id)))
|
||||
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id)))
|
||||
else:
|
||||
error_msg = 'Fehler beim Hinzufügen des Elements'
|
||||
if is_mobile:
|
||||
return jsonify({'success': False, 'message': error_msg}), 500
|
||||
else:
|
||||
flash(error_msg, 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
return redirect(url_for(success_redirect_endpoint))
|
||||
|
||||
|
||||
@app.route('/duplicate_item', methods=['POST'])
|
||||
@@ -6688,12 +6721,14 @@ def register():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
name = request.form['name']
|
||||
last_name = request.form['last-name']
|
||||
name = (request.form.get('name') or '').strip()
|
||||
last_name = (request.form.get('last-name') or '').strip()
|
||||
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
|
||||
use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
|
||||
is_student = bool(request.form.get('is_student')) if cfg.STUDENT_CARDS_MODULE_ENABLED else False
|
||||
student_card_id = us.normalize_student_card_id(request.form.get('student_card_id')) if cfg.STUDENT_CARDS_MODULE_ENABLED else ''
|
||||
max_borrow_days_raw = request.form.get('max_borrow_days') if cfg.STUDENT_CARDS_MODULE_ENABLED else None
|
||||
if not username or not password:
|
||||
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'))
|
||||
if us.get_user(username):
|
||||
@@ -6703,6 +6738,17 @@ def register():
|
||||
flash('Passwort ist zu schwach', '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'
|
||||
|
||||
max_borrow_days = None
|
||||
if is_student:
|
||||
if not student_card_id:
|
||||
@@ -6724,7 +6770,10 @@ def register():
|
||||
last_name,
|
||||
is_student=is_student,
|
||||
student_card_id=student_card_id if is_student else None,
|
||||
max_borrow_days=max_borrow_days
|
||||
max_borrow_days=max_borrow_days,
|
||||
permission_preset=permission_preset,
|
||||
action_permissions=action_permissions,
|
||||
page_permissions=page_permissions,
|
||||
)
|
||||
return redirect(url_for('home'))
|
||||
return render_template(
|
||||
|
||||
@@ -786,7 +786,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_manage_settings', True) %}
|
||||
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}">➕ Hochladen</a>
|
||||
</li>
|
||||
@@ -907,7 +907,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) and current_permissions.pages.get('library_admin', True) %}
|
||||
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a>
|
||||
</li>
|
||||
|
||||
@@ -843,7 +843,7 @@
|
||||
|
||||
const favoriteIds = new Set(data.favorites || []);
|
||||
window.currentFavorites = favoriteIds;
|
||||
try { sessionStorage.setItem('favoritesCache', JSON.stringify(Array.from(favoriteIds))); } catch(e){}
|
||||
try { sessionStorage.setItem('favoritesCache:' + ({{ session.get('username', '') | tojson }} || 'anon'), JSON.stringify(Array.from(favoriteIds))); } catch(e){}
|
||||
pageItems.forEach(item => {
|
||||
try {
|
||||
const card = document.createElement('div');
|
||||
@@ -4233,6 +4233,7 @@
|
||||
<script>
|
||||
let favoritesOnly = false;
|
||||
let tableViewMode = false;
|
||||
const favoritesCacheKey = 'favoritesCache:' + ({{ session.get('username', '') | tojson }} || 'anon');
|
||||
|
||||
function setViewModeState() {
|
||||
document.body.classList.toggle('table-view', tableViewMode);
|
||||
@@ -4263,7 +4264,7 @@ function toggleFavorite(id, btn, card){
|
||||
}
|
||||
if(!window.currentFavorites) window.currentFavorites = new Set();
|
||||
if(isFav) window.currentFavorites.add(id); else window.currentFavorites.delete(id);
|
||||
try { sessionStorage.setItem('favoritesCache', JSON.stringify(Array.from(window.currentFavorites))); } catch(e){}
|
||||
try { sessionStorage.setItem(favoritesCacheKey, JSON.stringify(Array.from(window.currentFavorites))); } catch(e){}
|
||||
})
|
||||
.catch(err=>console.error('Netzwerkfehler Favoriten', err));
|
||||
}
|
||||
@@ -4271,7 +4272,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
// Initialize favorites cache set if stored
|
||||
try {
|
||||
if(!window.currentFavorites){
|
||||
const cached = sessionStorage.getItem('favoritesCache');
|
||||
const cached = sessionStorage.getItem(favoritesCacheKey);
|
||||
if(cached){ window.currentFavorites = new Set(JSON.parse(cached)); }
|
||||
}
|
||||
} catch(e){}
|
||||
@@ -4310,7 +4311,7 @@ function openItemQuick(id){
|
||||
if(item && !item.error){
|
||||
// ensure favorites set available
|
||||
if(!window.currentFavorites){
|
||||
window.currentFavorites = new Set(JSON.parse(sessionStorage.getItem('favoritesCache')||'[]'));
|
||||
window.currentFavorites = new Set(JSON.parse(sessionStorage.getItem(favoritesCacheKey)||'[]'));
|
||||
}
|
||||
openItemModal(item);
|
||||
}
|
||||
|
||||
+216
-9
@@ -39,21 +39,31 @@
|
||||
<span class="input-icon">👤</span>
|
||||
<input type="text" id="username" name="username" placeholder="Geben Sie einen Benutzernamen ein" required>
|
||||
</div>
|
||||
<label for="name">Name</label>
|
||||
<label for="name">Vorname</label>
|
||||
<div class="input-container">
|
||||
<span class="input-icon">👤</span>
|
||||
<input type="text" id="name" name="name" placeholder="Geben Sie den Namen ein" required>
|
||||
<input type="text" id="name" name="name" placeholder="Geben Sie den Vornamen ein" required>
|
||||
</div>
|
||||
<label for="last-name">Nachname</label>
|
||||
<div class="input-container">
|
||||
<span class="input-icon">👤</span>
|
||||
<input type="text" id="last-name" name="last-name" placeholder="Geben Sie den Nachnamen ein" required>
|
||||
</div>
|
||||
<p class="anonymize-hint">Klarnamen werden nur zur Erzeugung eines Kuerzels (z.B. SimFri) verwendet und nicht als Klarname gespeichert.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<a class="richtlinen">Das Password muss mindestens 6 Zeichen beinhalten mit Sonderzeichen, groß und klein Buchstaben sowie Zahlen!</a>
|
||||
<div class="password-rules" id="password-rules" aria-live="polite">
|
||||
<p class="password-rules-title">Passwort-Anforderungen (live):</p>
|
||||
<ul>
|
||||
<li id="pw-rule-length" class="pw-rule">Mindestens 12 Zeichen</li>
|
||||
<li id="pw-rule-lower" class="pw-rule">Mindestens ein Kleinbuchstabe</li>
|
||||
<li id="pw-rule-upper" class="pw-rule">Mindestens ein Grossbuchstabe</li>
|
||||
<li id="pw-rule-digit" class="pw-rule">Mindestens eine Zahl</li>
|
||||
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<span class="input-icon">🔒</span>
|
||||
<input type="password" id="password" name="password" placeholder="Geben Sie ein sicheres Passwort ein" required>
|
||||
@@ -81,6 +91,43 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="permission-preset">Berechtigungs-Preset</label>
|
||||
<select id="permission-preset" name="permission_preset" class="form-select">
|
||||
{% for preset_key, preset in permission_presets.items() %}
|
||||
<option value="{{ preset_key }}">{{ preset.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label style="display:flex; align-items:center; gap:8px; margin-top:12px; color:#1f2937;">
|
||||
<input type="checkbox" id="use-custom-permissions" name="use_custom_permissions" style="width:auto;">
|
||||
Individuelle Berechtigungen statt Preset setzen
|
||||
</label>
|
||||
|
||||
<div id="custom-permissions" style="display:none; margin-top:12px;">
|
||||
<div class="permission-panels">
|
||||
<div class="permission-panel">
|
||||
<h4>Aktionsrechte</h4>
|
||||
{% for action_key, action_label in permission_action_options %}
|
||||
<label class="permission-check">
|
||||
<input type="checkbox" class="permission-action-checkbox" name="action_{{ action_key }}">
|
||||
<span>{{ action_label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="permission-panel">
|
||||
<h4>Seitenrechte</h4>
|
||||
{% for endpoint_name, endpoint_label in permission_page_options %}
|
||||
<label class="permission-check">
|
||||
<input type="checkbox" class="permission-page-checkbox" name="page_{{ endpoint_name }}">
|
||||
<span>{{ endpoint_label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-actions">
|
||||
<button type="submit" class="action-button register-button">Benutzer registrieren</button>
|
||||
</div>
|
||||
@@ -174,7 +221,8 @@ body {
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
input[type="password"],
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem 0.8rem 3rem;
|
||||
border: 1px solid #ddd;
|
||||
@@ -185,12 +233,17 @@ input[type="password"] {
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus {
|
||||
input[type="password"]:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
@@ -308,14 +361,168 @@ input::placeholder {
|
||||
}
|
||||
}
|
||||
|
||||
.richtlinen{
|
||||
color: #ec0920;
|
||||
.password-rules {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.password-rules-title {
|
||||
margin: 0 0 8px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.password-rules ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pw-rule {
|
||||
position: relative;
|
||||
padding-left: 22px;
|
||||
margin: 5px 0;
|
||||
color: #b91c1c;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.pw-rule::before {
|
||||
content: '✗';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pw-rule.ok {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.pw-rule.ok::before {
|
||||
content: '✓';
|
||||
}
|
||||
|
||||
.anonymize-hint {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
color: #0c4a6e;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.permission-panels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.permission-panel {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.permission-panel h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1rem;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.permission-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if student_cards_module_enabled %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const permissionPresets = {{ permission_presets | tojson }};
|
||||
const presetSelect = document.getElementById('permission-preset');
|
||||
const useCustomPermissions = document.getElementById('use-custom-permissions');
|
||||
const customPermissions = document.getElementById('custom-permissions');
|
||||
|
||||
function applyPresetToPermissionForm(presetKey) {
|
||||
const preset = permissionPresets[presetKey] || {};
|
||||
const actionDefaults = preset.actions || {};
|
||||
const pageDefaults = preset.pages || {};
|
||||
|
||||
document.querySelectorAll('.permission-action-checkbox').forEach(function (checkbox) {
|
||||
const key = checkbox.name.replace('action_', '');
|
||||
checkbox.checked = !!actionDefaults[key];
|
||||
});
|
||||
|
||||
document.querySelectorAll('.permission-page-checkbox').forEach(function (checkbox) {
|
||||
const key = checkbox.name.replace('page_', '');
|
||||
checkbox.checked = !!pageDefaults[key];
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCustomPermissions() {
|
||||
if (!useCustomPermissions || !customPermissions) {
|
||||
return;
|
||||
}
|
||||
customPermissions.style.display = useCustomPermissions.checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (presetSelect) {
|
||||
presetSelect.addEventListener('change', function () {
|
||||
applyPresetToPermissionForm(this.value);
|
||||
});
|
||||
applyPresetToPermissionForm(presetSelect.value);
|
||||
}
|
||||
|
||||
if (useCustomPermissions) {
|
||||
useCustomPermissions.addEventListener('change', toggleCustomPermissions);
|
||||
toggleCustomPermissions();
|
||||
}
|
||||
|
||||
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) {
|
||||
passwordInput.addEventListener('input', updatePasswordRules);
|
||||
passwordInput.addEventListener('blur', updatePasswordRules);
|
||||
updatePasswordRules();
|
||||
}
|
||||
|
||||
{% if student_cards_module_enabled %}
|
||||
const studentCheckbox = document.getElementById('is-student');
|
||||
const studentFields = document.getElementById('student-fields');
|
||||
const studentCardInput = document.getElementById('student-card-id');
|
||||
@@ -332,7 +539,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
studentCheckbox.addEventListener('change', toggleStudentFields);
|
||||
toggleStudentFields();
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -446,5 +446,13 @@
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: 1998 !important;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 1999 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
+26
-3
@@ -234,6 +234,10 @@ def get_effective_permissions(username):
|
||||
if not user:
|
||||
return build_default_permission_payload('standard_user')
|
||||
|
||||
# Admin users always have full access, independent of custom presets.
|
||||
if bool(user.get('Admin', False)):
|
||||
return build_default_permission_payload('full_access')
|
||||
|
||||
preset_key = user.get('PermissionPreset') or 'standard_user'
|
||||
payload = build_default_permission_payload(preset_key)
|
||||
payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions'])
|
||||
@@ -376,7 +380,18 @@ def check_nm_pwd(username, password):
|
||||
return user
|
||||
|
||||
|
||||
def add_user(username, password, name, last_name, is_student=False, student_card_id=None, max_borrow_days=None):
|
||||
def add_user(
|
||||
username,
|
||||
password,
|
||||
name='',
|
||||
last_name='',
|
||||
is_student=False,
|
||||
student_card_id=None,
|
||||
max_borrow_days=None,
|
||||
permission_preset='standard_user',
|
||||
action_permissions=None,
|
||||
page_permissions=None,
|
||||
):
|
||||
"""
|
||||
Add a new user to the database.
|
||||
|
||||
@@ -392,9 +407,17 @@ def add_user(username, password, name, last_name, is_student=False, student_card
|
||||
users = db['users']
|
||||
if not check_password_strength(password):
|
||||
return False
|
||||
permission_defaults = build_default_permission_payload('standard_user')
|
||||
permission_defaults = build_default_permission_payload(permission_preset)
|
||||
if isinstance(action_permissions, dict):
|
||||
for key, value in action_permissions.items():
|
||||
permission_defaults['actions'][str(key)] = bool(value)
|
||||
if isinstance(page_permissions, dict):
|
||||
for key, value in page_permissions.items():
|
||||
permission_defaults['pages'][str(key)] = bool(value)
|
||||
|
||||
name_alias = build_name_synonym(name, last_name)
|
||||
alias_first = name if str(name or '').strip() else username
|
||||
alias_last = last_name if str(last_name or '').strip() else ''
|
||||
name_alias = build_name_synonym(alias_first, alias_last)
|
||||
|
||||
user_doc = {
|
||||
'Username': username,
|
||||
|
||||
Reference in New Issue
Block a user