rename benutzer to users

add role management to media page
This commit is contained in:
RobbStarkAustria
2025-10-16 17:57:06 +00:00
parent a7df3c2708
commit 7b38b49598
10 changed files with 116 additions and 5 deletions

View File

@@ -34,6 +34,7 @@
"version": "2025.1.0-alpha.10", "version": "2025.1.0-alpha.10",
"date": "2025-10-15", "date": "2025-10-15",
"changes": [ "changes": [
"🔐 Auth: Login und Benutzerverwaltung implementiert (rollenbasiert, persistente Sitzungen).",
"✨ UI: Benutzer-Menü oben rechts DropDownButton mit Benutzername/Rolle; Einträge: Profil und Abmelden.", "✨ UI: Benutzer-Menü oben rechts DropDownButton mit Benutzername/Rolle; Einträge: Profil und Abmelden.",
"🧩 Frontend: Syncfusion SplitButtons integriert (react-splitbuttons) und Vite-Konfiguration für Pre-Bundling ergänzt.", "🧩 Frontend: Syncfusion SplitButtons integriert (react-splitbuttons) und Vite-Konfiguration für Pre-Bundling ergänzt.",
"🐛 Fix: Import-Fehler @syncfusion/ej2-react-splitbuttons Anleitung in README hinzugefügt (optimizeDeps + Volume-Reset)." "🐛 Fix: Import-Fehler @syncfusion/ej2-react-splitbuttons Anleitung in README hinzugefügt (optimizeDeps + Volume-Reset)."

View File

@@ -44,7 +44,7 @@ import Ressourcen from './ressourcen';
import Infoscreens from './clients'; import Infoscreens from './clients';
import Infoscreen_groups from './infoscreen_groups'; import Infoscreen_groups from './infoscreen_groups';
import Media from './media'; import Media from './media';
import Benutzer from './benutzer'; import Benutzer from './users';
import Einstellungen from './settings'; import Einstellungen from './settings';
import SetupMode from './SetupMode'; import SetupMode from './SetupMode';
import Programminfo from './programminfo'; import Programminfo from './programminfo';

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useAuth } from '../useAuth';
import { DialogComponent } from '@syncfusion/ej2-react-popups'; import { DialogComponent } from '@syncfusion/ej2-react-popups';
import { import {
FileManagerComponent, FileManagerComponent,
@@ -19,6 +20,8 @@ type CustomSelectUploadEventModalProps = {
const CustomSelectUploadEventModal: React.FC<CustomSelectUploadEventModalProps> = props => { const CustomSelectUploadEventModal: React.FC<CustomSelectUploadEventModalProps> = props => {
const { open, onClose, onSelect } = props; const { open, onClose, onSelect } = props;
const { user } = useAuth();
const isSuperadmin = useMemo(() => user?.role === 'superadmin', [user]);
const [selectedFile, setSelectedFile] = useState<{ const [selectedFile, setSelectedFile] = useState<{
id: string; id: string;
@@ -63,6 +66,23 @@ const CustomSelectUploadEventModal: React.FC<CustomSelectUploadEventModalProps>
} }
}; };
type FileItem = { name: string; isFile: boolean };
type ReadSuccessArgs = { action: string; result?: { files?: FileItem[] } };
type FileOpenArgs = { fileDetails?: FileItem; cancel?: boolean };
const handleSuccess = (args: ReadSuccessArgs) => {
if (isSuperadmin) return;
if (args && args.action === 'read' && args.result && Array.isArray(args.result.files)) {
args.result.files = args.result.files.filter((f: FileItem) => !(f.name === 'converted' && !f.isFile));
}
};
const handleFileOpen = (args: FileOpenArgs) => {
if (!isSuperadmin && args && args.fileDetails && args.fileDetails.name === 'converted' && !args.fileDetails.isFile) {
args.cancel = true;
}
};
return ( return (
<DialogComponent <DialogComponent
target="#root" target="#root"
@@ -84,6 +104,9 @@ const CustomSelectUploadEventModal: React.FC<CustomSelectUploadEventModalProps>
)} )}
> >
<FileManagerComponent <FileManagerComponent
cssClass="e-bigger media-icons-xl"
success={handleSuccess}
fileOpen={handleFileOpen}
ajaxSettings={{ ajaxSettings={{
url: hostUrl + 'operations', url: hostUrl + 'operations',
getImageUrl: hostUrl + 'get-image', getImageUrl: hostUrl + 'get-image',

View File

@@ -1,5 +1,7 @@
/* Tailwind removed: base/components/utilities directives no longer used. */ /* Tailwind removed: base/components/utilities directives no longer used. */
/* Custom overrides moved to theme-overrides.css to load after Syncfusion styles */
/* :root { /* :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5; line-height: 1.5;

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from './useAuth'; import { useAuth } from './useAuth';
export default function Login() { export default function Login() {
@@ -7,14 +8,16 @@ export default function Login() {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
const isDev = import.meta.env.MODE !== 'production'; const isDev = import.meta.env.MODE !== 'production';
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setMessage(null); setMessage(null);
try { try {
await login(username, password); await login(username, password);
// Browser will stay on /login; App's route gate will redirect to '/'
setMessage('Login erfolgreich'); setMessage('Login erfolgreich');
// Redirect to dashboard after successful login
navigate('/');
} catch (err) { } catch (err) {
setMessage(err instanceof Error ? err.message : 'Login fehlgeschlagen'); setMessage(err instanceof Error ? err.message : 'Login fehlgeschlagen');
} }

View File

@@ -21,6 +21,7 @@ import '@syncfusion/ej2-lists/styles/material3.css';
import '@syncfusion/ej2-calendars/styles/material3.css'; import '@syncfusion/ej2-calendars/styles/material3.css';
import '@syncfusion/ej2-splitbuttons/styles/material3.css'; import '@syncfusion/ej2-splitbuttons/styles/material3.css';
import '@syncfusion/ej2-icons/styles/material3.css'; import '@syncfusion/ej2-icons/styles/material3.css';
import './theme-overrides.css';
// Setze hier deinen Lizenzschlüssel ein // Setze hier deinen Lizenzschlüssel ein
registerLicense( registerLicense(

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import CustomMediaInfoPanel from './components/CustomMediaInfoPanel'; import CustomMediaInfoPanel from './components/CustomMediaInfoPanel';
import { import {
FileManagerComponent, FileManagerComponent,
@@ -7,10 +7,13 @@ import {
DetailsView, DetailsView,
Toolbar, Toolbar,
} from '@syncfusion/ej2-react-filemanager'; } from '@syncfusion/ej2-react-filemanager';
import { useAuth } from './useAuth';
const hostUrl = '/api/eventmedia/filemanager/'; // Dein Backend-Endpunkt für FileManager const hostUrl = '/api/eventmedia/filemanager/'; // Dein Backend-Endpunkt für FileManager
const Media: React.FC = () => { const Media: React.FC = () => {
const { user } = useAuth();
const isSuperadmin = useMemo(() => user?.role === 'superadmin', [user]);
// State für die angezeigten Dateidetails // State für die angezeigten Dateidetails
const [fileDetails] = useState<null | { const [fileDetails] = useState<null | {
name: string; name: string;
@@ -43,6 +46,25 @@ const Media: React.FC = () => {
} }
}, [viewMode]); }, [viewMode]);
type FileItem = { name: string; isFile: boolean };
type ReadSuccessArgs = { action: string; result?: { files?: FileItem[] } };
type FileOpenArgs = { fileDetails?: FileItem; cancel?: boolean };
// Hide "converted" for non-superadmins after data load
const handleSuccess = (args: ReadSuccessArgs) => {
if (isSuperadmin) return;
if (args && args.action === 'read' && args.result && Array.isArray(args.result.files)) {
args.result.files = args.result.files.filter((f: FileItem) => !(f.name === 'converted' && !f.isFile));
}
};
// Prevent opening the "converted" folder for non-superadmins
const handleFileOpen = (args: FileOpenArgs) => {
if (!isSuperadmin && args && args.fileDetails && args.fileDetails.name === 'converted' && !args.fileDetails.isFile) {
args.cancel = true;
}
};
return ( return (
<div> <div>
<h2 className="text-xl font-bold mb-4">Medien</h2> <h2 className="text-xl font-bold mb-4">Medien</h2>
@@ -65,6 +87,9 @@ const Media: React.FC = () => {
{/* Debug-Ausgabe entfernt, da ReactNode erwartet wird */} {/* Debug-Ausgabe entfernt, da ReactNode erwartet wird */}
<FileManagerComponent <FileManagerComponent
ref={fileManagerRef} ref={fileManagerRef}
cssClass="e-bigger media-icons-xl"
success={handleSuccess}
fileOpen={handleFileOpen}
ajaxSettings={{ ajaxSettings={{
url: hostUrl + 'operations', url: hostUrl + 'operations',
getImageUrl: hostUrl + 'get-image', getImageUrl: hostUrl + 'get-image',

View File

@@ -0,0 +1,15 @@
/* FileManager icon size overrides (loaded after Syncfusion styles) */
.e-filemanager.media-icons-xl .e-large-icons .e-list-icon {
font-size: 40px; /* default ~24px */
}
.e-filemanager.media-icons-xl .e-large-icons .e-fe-folder,
.e-filemanager.media-icons-xl .e-large-icons .e-fe-file {
font-size: 40px;
}
/* Details (grid) view icons */
.e-filemanager.media-icons-xl .e-fe-grid-icon .e-fe-folder,
.e-filemanager.media-icons-xl .e-fe-grid-icon .e-fe-file {
font-size: 24px;
}

View File

@@ -38,7 +38,18 @@ def filemanager_operations():
print(action, path, name, new_name, target_path, full_path) # Debug-Ausgabe print(action, path, name, new_name, target_path, full_path) # Debug-Ausgabe
# Superadmin-only protection for the converted folder
from flask import session as flask_session
user_role = flask_session.get('role')
is_superadmin = user_role == 'superadmin'
# Normalize path for checks
norm_path = os.path.normpath('/' + path.lstrip('/'))
under_converted = norm_path == '/converted' or norm_path.startswith('/converted/')
if action == 'read': if action == 'read':
# Block listing inside converted for non-superadmins
if under_converted and not is_superadmin:
return jsonify({'files': [], 'cwd': {'name': os.path.basename(full_path), 'path': path}})
# List files and folders # List files and folders
items = [] items = []
session = Session() session = Session()
@@ -61,7 +72,9 @@ def filemanager_operations():
item['dateModified'] = entry.stat().st_mtime item['dateModified'] = entry.stat().st_mtime
else: else:
item['dateModified'] = entry.stat().st_mtime item['dateModified'] = entry.stat().st_mtime
items.append(item) # Hide the converted folder at root for non-superadmins
if not (not is_superadmin and not entry.is_file() and entry.name == 'converted' and (norm_path == '/' or norm_path == '')):
items.append(item)
session.close() session.close()
return jsonify({'files': items, 'cwd': {'name': os.path.basename(full_path), 'path': path}}) return jsonify({'files': items, 'cwd': {'name': os.path.basename(full_path), 'path': path}})
@@ -90,6 +103,8 @@ def filemanager_operations():
session.close() session.close()
return jsonify({'details': details}) return jsonify({'details': details})
elif action == 'delete': elif action == 'delete':
if under_converted and not is_superadmin:
return jsonify({'error': 'Insufficient permissions'}), 403
for item in request.form.getlist('names[]'): for item in request.form.getlist('names[]'):
item_path = os.path.join(full_path, item) item_path = os.path.join(full_path, item)
if os.path.isdir(item_path): if os.path.isdir(item_path):
@@ -98,16 +113,23 @@ def filemanager_operations():
os.remove(item_path) os.remove(item_path)
return jsonify({'success': True}) return jsonify({'success': True})
elif action == 'rename': elif action == 'rename':
if under_converted and not is_superadmin:
return jsonify({'error': 'Insufficient permissions'}), 403
src = os.path.join(full_path, name) src = os.path.join(full_path, name)
dst = os.path.join(full_path, new_name) dst = os.path.join(full_path, new_name)
os.rename(src, dst) os.rename(src, dst)
return jsonify({'success': True}) return jsonify({'success': True})
elif action == 'move': elif action == 'move':
# Prevent moving into converted if not superadmin
if (target_path and target_path.strip('/').split('/')[0] == 'converted') and not is_superadmin:
return jsonify({'error': 'Insufficient permissions'}), 403
src = os.path.join(full_path, name) src = os.path.join(full_path, name)
dst = os.path.join(MEDIA_ROOT, target_path.lstrip('/'), name) dst = os.path.join(MEDIA_ROOT, target_path.lstrip('/'), name)
os.rename(src, dst) os.rename(src, dst)
return jsonify({'success': True}) return jsonify({'success': True})
elif action == 'create': elif action == 'create':
if under_converted and not is_superadmin:
return jsonify({'error': 'Insufficient permissions'}), 403
os.makedirs(os.path.join(full_path, name), exist_ok=True) os.makedirs(os.path.join(full_path, name), exist_ok=True)
return jsonify({'success': True}) return jsonify({'success': True})
else: else:
@@ -122,6 +144,12 @@ def filemanager_upload():
session = Session() session = Session()
# Korrigiert: Erst aus request.form, dann aus request.args lesen # Korrigiert: Erst aus request.form, dann aus request.args lesen
path = request.form.get('path') or request.args.get('path', '/') path = request.form.get('path') or request.args.get('path', '/')
from flask import session as flask_session
user_role = flask_session.get('role')
is_superadmin = user_role == 'superadmin'
norm_path = os.path.normpath('/' + path.lstrip('/'))
if (norm_path == '/converted' or norm_path.startswith('/converted/')) and not is_superadmin:
return jsonify({'error': 'Insufficient permissions'}), 403
upload_path = os.path.join(MEDIA_ROOT, path.lstrip('/')) upload_path = os.path.join(MEDIA_ROOT, path.lstrip('/'))
os.makedirs(upload_path, exist_ok=True) os.makedirs(upload_path, exist_ok=True)
for file in request.files.getlist('uploadFiles'): for file in request.files.getlist('uploadFiles'):
@@ -184,9 +212,16 @@ def filemanager_upload():
@eventmedia_bp.route('/filemanager/download', methods=['GET']) @eventmedia_bp.route('/filemanager/download', methods=['GET'])
def filemanager_download(): def filemanager_download():
path = request.args.get('path', '/') path = request.args.get('path', '/')
from flask import session as flask_session
user_role = flask_session.get('role')
is_superadmin = user_role == 'superadmin'
norm_path = os.path.normpath('/' + path.lstrip('/'))
names = request.args.getlist('names[]') names = request.args.getlist('names[]')
# Nur Einzel-Download für Beispiel # Nur Einzel-Download für Beispiel
if names: if names:
# Block access to converted for non-superadmins
if (norm_path == '/converted' or norm_path.startswith('/converted/')) and not is_superadmin:
return jsonify({'error': 'Insufficient permissions'}), 403
file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'), names[0]) file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'), names[0])
return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True)
return jsonify({'error': 'No file specified'}), 400 return jsonify({'error': 'No file specified'}), 400
@@ -197,6 +232,12 @@ def filemanager_download():
@eventmedia_bp.route('/filemanager/get-image', methods=['GET']) @eventmedia_bp.route('/filemanager/get-image', methods=['GET'])
def filemanager_get_image(): def filemanager_get_image():
path = request.args.get('path', '/') path = request.args.get('path', '/')
from flask import session as flask_session
user_role = flask_session.get('role')
is_superadmin = user_role == 'superadmin'
norm_path = os.path.normpath('/' + path.lstrip('/'))
if (norm_path == '/converted' or norm_path.startswith('/converted/')) and not is_superadmin:
return jsonify({'error': 'Insufficient permissions'}), 403
file_path = os.path.join(MEDIA_ROOT, path.lstrip('/')) file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'))
return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path)) return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path))