rename benutzer to users
add role management to media page
This commit is contained in:
@@ -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)."
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
15
dashboard/src/theme-overrides.css
Normal file
15
dashboard/src/theme-overrides.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user