Compare commits

...

9 Commits

8 changed files with 1553 additions and 15 deletions

Binary file not shown.

Binary file not shown.

855
app.py
View File

@@ -2,7 +2,10 @@
# -*- coding: utf-8 -*-
import os
import logging
import traceback
from datetime import datetime, timedelta, timezone
from functools import wraps
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_sqlalchemy import SQLAlchemy
@@ -26,9 +29,10 @@ import os
# Modelle importieren
from models import (
db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating,
RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote,
node_thought_association, user_thought_bookmark, node_relationship
db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating,
RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote,
node_thought_association, user_thought_bookmark, node_relationship,
MindmapShare, PermissionType
)
# Lade .env-Datei
@@ -47,6 +51,87 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads'))
# Logger-Konfiguration
log_level = os.environ.get('LOG_LEVEL', 'INFO').upper()
log_dir = os.path.join(basedir, 'logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'app.log')
# Handler für Datei-Logs
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(getattr(logging, log_level))
# Handler für Konsolen-Logs
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
console_handler.setLevel(getattr(logging, log_level))
# Konfiguriere App-Logger
app.logger.addHandler(file_handler)
app.logger.addHandler(console_handler)
app.logger.setLevel(getattr(logging, log_level))
app.logger.info('Anwendung gestartet')
# Einheitliches Fehlerbehandlungssystem
class ErrorHandler:
"""Zentralisierte Fehlerbehandlung für die gesamte Anwendung"""
@staticmethod
def log_exception(e, endpoint=None, code=500):
"""Protokolliert eine Ausnahme mit Kontext-Informationen"""
# Stack-Trace für Debugging
trace = traceback.format_exc()
# Benutzer-Informationen (wenn verfügbar)
user_info = f"User: {current_user.id} ({current_user.username})" if current_user and current_user.is_authenticated else "Nicht angemeldet"
# Request-Informationen
request_info = f"Endpoint: {endpoint or request.path}, Method: {request.method}, IP: {request.remote_addr}"
# Vollständige Log-Nachricht
app.logger.error(f"Fehler {code}: {str(e)}\n{request_info}\n{user_info}\n{trace}")
@staticmethod
def api_error(message, code=400, details=None):
"""Erstellt eine standardisierte API-Fehlerantwort"""
response = {
'success': False,
'error': {
'code': code,
'message': message
}
}
if details:
response['error']['details'] = details
return jsonify(response), code
@staticmethod
def handle_exception(e, is_api_request=False):
"""Behandelt eine Ausnahme basierend auf ihrem Typ"""
# Bestimme, ob es sich um eine API-Anfrage handelt
if is_api_request is None:
is_api_request = request.path.startswith('/api/') or request.headers.get('Accept') == 'application/json'
# Protokolliere die Ausnahme
ErrorHandler.log_exception(e)
# Erstelle benutzerfreundliche Fehlermeldung
user_message = "Ein unerwarteter Fehler ist aufgetreten."
if is_api_request:
# API-Antwort für Ausnahme
return ErrorHandler.api_error(user_message, 500)
else:
# Web-Antwort für Ausnahme (mit Flash-Nachricht)
flash(user_message, 'error')
return render_template('errors/500.html'), 500
# Globaler Exception-Handler
@app.errorhandler(Exception)
def handle_unhandled_exception(e):
"""Fängt alle unbehandelten Ausnahmen ab"""
return ErrorHandler.handle_exception(e)
# OpenAI API-Konfiguration
api_key = os.environ.get("OPENAI_API_KEY", "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")
@@ -776,11 +861,17 @@ def api_get_user_mindmaps():
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['GET'])
@login_required
@handle_api_exception
def api_get_user_mindmap_detail(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
# Bestehende Logik von get_user_mindmap kann hier wiederverwendet oder angepasst werden
# Für eine einfache Detailansicht:
return jsonify({
# Berechtigungsprüfung (mindestens READ-Zugriff)
has_permission, error_msg = check_mindmap_permission(mindmap_id, "READ")
if not has_permission:
return ErrorHandler.api_error(error_msg, 403)
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Logik für eine detaillierte Ansicht
result = {
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
@@ -788,18 +879,44 @@ def api_get_user_mindmap_detail(mindmap_id):
'user_id': mindmap.user_id,
'created_at': mindmap.created_at.isoformat(),
'last_modified': mindmap.last_modified.isoformat(),
# Hier könnten auch Knoten und Notizen hinzugefügt werden, ähnlich wie in get_user_mindmap
})
'is_owner': mindmap.user_id == current_user.id
}
# Berechtigungsinformationen hinzufügen, falls nicht der Eigentümer
if mindmap.user_id != current_user.id:
share = MindmapShare.query.filter_by(
mindmap_id=mindmap_id,
shared_with_id=current_user.id
).first()
if share:
result['permission'] = share.permission_type.name
result['owner'] = {
'id': mindmap.user_id,
'username': User.query.get(mindmap.user_id).username
}
return jsonify(result)
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['PUT'])
@login_required
@handle_api_exception
def api_update_user_mindmap(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
# Berechtigungsprüfung (mindestens EDIT-Zugriff)
has_permission, error_msg = check_mindmap_permission(mindmap_id, "EDIT")
if not has_permission:
return ErrorHandler.api_error(error_msg, 403)
mindmap = UserMindmap.query.get_or_404(mindmap_id)
data = request.get_json()
# Grundlegende Informationen aktualisieren
mindmap.name = data.get('name', mindmap.name)
mindmap.description = data.get('description', mindmap.description)
mindmap.is_private = data.get('is_private', mindmap.is_private)
# is_private kann nur vom Eigentümer geändert werden
if mindmap.user_id == current_user.id and 'is_private' in data:
mindmap.is_private = data.get('is_private')
db.session.commit()
return jsonify({
@@ -807,16 +924,31 @@ def api_update_user_mindmap(mindmap_id):
'name': mindmap.name,
'description': mindmap.description,
'is_private': mindmap.is_private,
'last_modified': mindmap.last_modified.isoformat()
'last_modified': mindmap.last_modified.isoformat(),
'success': True
})
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['DELETE'])
@login_required
@handle_api_exception
def api_delete_user_mindmap(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
# Nur der Eigentümer kann eine Mindmap löschen
mindmap = UserMindmap.query.get_or_404(mindmap_id)
if mindmap.user_id != current_user.id:
return ErrorHandler.api_error("Nur der Eigentümer kann eine Mindmap löschen.", 403)
# Alle Freigaben löschen
MindmapShare.query.filter_by(mindmap_id=mindmap_id).delete()
# Mindmap löschen
db.session.delete(mindmap)
db.session.commit()
return jsonify({'message': 'Mindmap erfolgreich gelöscht'}), 200
return jsonify({
'success': True,
'message': 'Mindmap erfolgreich gelöscht'
}), 200
# API-Endpunkte für Mindmap-Daten (öffentlich und benutzerspezifisch)
@app.route('/api/mindmap/public')
@@ -1627,20 +1759,78 @@ def get_dark_mode():
# Fehlerhandler
@app.errorhandler(404)
def page_not_found(e):
"""404 Fehler - Seite nicht gefunden"""
ErrorHandler.log_exception(e, code=404)
is_api_request = request.path.startswith('/api/')
if is_api_request:
return ErrorHandler.api_error("Die angeforderte Ressource wurde nicht gefunden.", 404)
return render_template('errors/404.html'), 404
@app.errorhandler(403)
def forbidden(e):
"""403 Fehler - Zugriff verweigert"""
ErrorHandler.log_exception(e, code=403)
is_api_request = request.path.startswith('/api/')
if is_api_request:
return ErrorHandler.api_error("Sie haben keine Berechtigung, auf diese Ressource zuzugreifen.", 403)
return render_template('errors/403.html'), 403
@app.errorhandler(500)
def internal_server_error(e):
"""500 Fehler - Interner Serverfehler"""
ErrorHandler.log_exception(e, code=500)
is_api_request = request.path.startswith('/api/')
if is_api_request:
return ErrorHandler.api_error("Ein interner Serverfehler ist aufgetreten.", 500)
return render_template('errors/500.html'), 500
@app.errorhandler(429)
def too_many_requests(e):
"""429 Fehler - Zu viele Anfragen"""
ErrorHandler.log_exception(e, code=429)
is_api_request = request.path.startswith('/api/')
if is_api_request:
return ErrorHandler.api_error("Zu viele Anfragen. Bitte versuchen Sie es später erneut.", 429)
return render_template('errors/429.html'), 429
@app.errorhandler(400)
def bad_request(e):
"""400 Fehler - Ungültige Anfrage"""
ErrorHandler.log_exception(e, code=400)
is_api_request = request.path.startswith('/api/')
if is_api_request:
return ErrorHandler.api_error("Die Anfrage konnte nicht verarbeitet werden.", 400)
flash("Die Anfrage konnte nicht verarbeitet werden. Bitte überprüfen Sie Ihre Eingaben.", "error")
return render_template('errors/400.html', error=str(e)), 400
@app.errorhandler(401)
def unauthorized(e):
"""401 Fehler - Nicht autorisiert"""
ErrorHandler.log_exception(e, code=401)
is_api_request = request.path.startswith('/api/')
if is_api_request:
return ErrorHandler.api_error("Authentifizierung erforderlich.", 401)
flash("Sie müssen sich anmelden, um auf diese Seite zuzugreifen.", "error")
return redirect(url_for('login'))
# Hilfsfunktion zur Fehlerbehandlung in API-Endpunkten
def handle_api_exception(func):
"""Decorator für API-Endpunkte zur einheitlichen Fehlerbehandlung"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
# Log und API-Fehler zurückgeben
return ErrorHandler.handle_exception(e, is_api_request=True)
return wrapper
# OpenAI-Integration für KI-Assistenz
@app.route('/api/assistant', methods=['POST'])
def chat_with_assistant():
@@ -1883,6 +2073,255 @@ def reload_env():
'message': f'Fehler beim Neuladen der Umgebungsvariablen: {str(e)}'
}), 500
# Berechtigungsverwaltung für Mindmaps
@app.route('/api/mindmap/<int:mindmap_id>/shares', methods=['GET'])
@login_required
@handle_api_exception
def get_mindmap_shares(mindmap_id):
"""Listet alle Benutzer auf, mit denen eine Mindmap geteilt wurde."""
# Überprüfen, ob die Mindmap dem Benutzer gehört
mindmap = UserMindmap.query.get_or_404(mindmap_id)
if mindmap.user_id != current_user.id:
return ErrorHandler.api_error("Sie haben keine Berechtigung, die Freigaben dieser Mindmap einzusehen.", 403)
# Alle Freigaben für diese Mindmap abrufen
shares = MindmapShare.query.filter_by(mindmap_id=mindmap_id).all()
result = []
for share in shares:
# Benutzerinformationen abrufen
shared_with_user = User.query.get(share.shared_with_id)
if shared_with_user:
result.append({
'id': share.id,
'user_id': shared_with_user.id,
'username': shared_with_user.username,
'email': shared_with_user.email,
'permission': share.permission_type.name,
'created_at': share.created_at.isoformat(),
'last_accessed': share.last_accessed.isoformat() if share.last_accessed else None
})
return jsonify({
'success': True,
'shares': result
})
@app.route('/api/mindmap/<int:mindmap_id>/share', methods=['POST'])
@login_required
@handle_api_exception
def share_mindmap(mindmap_id):
"""Teilt eine Mindmap mit einem anderen Benutzer."""
# Überprüfen, ob die Mindmap dem Benutzer gehört
mindmap = UserMindmap.query.get_or_404(mindmap_id)
if mindmap.user_id != current_user.id:
return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Mindmap zu teilen.", 403)
data = request.json
if not data or 'email' not in data or 'permission' not in data:
return ErrorHandler.api_error("E-Mail-Adresse und Berechtigungstyp sind erforderlich.", 400)
# Benutzer anhand der E-Mail-Adresse finden
user_to_share_with = User.query.filter_by(email=data['email']).first()
if not user_to_share_with:
return ErrorHandler.api_error("Kein Benutzer mit dieser E-Mail-Adresse gefunden.", 404)
# Prüfen, ob der Benutzer versucht, mit sich selbst zu teilen
if user_to_share_with.id == current_user.id:
return ErrorHandler.api_error("Sie können die Mindmap nicht mit sich selbst teilen.", 400)
# Prüfen, ob die Mindmap bereits mit diesem Benutzer geteilt wurde
existing_share = MindmapShare.query.filter_by(
mindmap_id=mindmap_id,
shared_with_id=user_to_share_with.id
).first()
# Berechtigungstyp validieren und konvertieren
try:
permission_type = PermissionType[data['permission']]
except (KeyError, ValueError):
return ErrorHandler.api_error(
"Ungültiger Berechtigungstyp. Erlaubte Werte sind: READ, EDIT, ADMIN.",
400
)
if existing_share:
# Wenn bereits geteilt, aktualisiere die Berechtigungen
existing_share.permission_type = permission_type
message = "Berechtigungen erfolgreich aktualisiert."
else:
# Wenn noch nicht geteilt, erstelle eine neue Freigabe
new_share = MindmapShare(
mindmap_id=mindmap_id,
shared_by_id=current_user.id,
shared_with_id=user_to_share_with.id,
permission_type=permission_type
)
db.session.add(new_share)
message = "Mindmap erfolgreich geteilt."
# Wenn die Mindmap bisher privat war, mache sie jetzt nicht mehr privat
if mindmap.is_private:
mindmap.is_private = False
db.session.commit()
return jsonify({
'success': True,
'message': message
})
@app.route('/api/mindmap/shares/<int:share_id>', methods=['PUT'])
@login_required
@handle_api_exception
def update_mindmap_share(share_id):
"""Aktualisiert die Berechtigungen für eine geteilte Mindmap."""
# Freigabe finden
share = MindmapShare.query.get_or_404(share_id)
# Prüfen, ob der Benutzer der Eigentümer der Mindmap ist
mindmap = UserMindmap.query.get(share.mindmap_id)
if not mindmap or mindmap.user_id != current_user.id:
return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Freigabe zu ändern.", 403)
data = request.json
if not data or 'permission' not in data:
return ErrorHandler.api_error("Berechtigungstyp ist erforderlich.", 400)
# Berechtigungstyp validieren und konvertieren
try:
permission_type = PermissionType[data['permission']]
except (KeyError, ValueError):
return ErrorHandler.api_error(
"Ungültiger Berechtigungstyp. Erlaubte Werte sind: READ, EDIT, ADMIN.",
400
)
# Berechtigungen aktualisieren
share.permission_type = permission_type
db.session.commit()
return jsonify({
'success': True,
'message': "Berechtigungen erfolgreich aktualisiert."
})
@app.route('/api/mindmap/shares/<int:share_id>', methods=['DELETE'])
@login_required
@handle_api_exception
def revoke_mindmap_share(share_id):
"""Widerruft die Freigabe einer Mindmap für einen Benutzer."""
# Freigabe finden
share = MindmapShare.query.get_or_404(share_id)
# Prüfen, ob der Benutzer der Eigentümer der Mindmap ist
mindmap = UserMindmap.query.get(share.mindmap_id)
if not mindmap or mindmap.user_id != current_user.id:
return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Freigabe zu widerrufen.", 403)
# Freigabe löschen
db.session.delete(share)
# Prüfen, ob dies die letzte Freigabe war und ggf. Mindmap wieder privat setzen
remaining_shares = MindmapShare.query.filter_by(mindmap_id=mindmap.id).count()
if remaining_shares == 0:
mindmap.is_private = True
db.session.commit()
return jsonify({
'success': True,
'message': "Freigabe erfolgreich widerrufen."
})
@app.route('/api/mindmaps/shared-with-me', methods=['GET'])
@login_required
@handle_api_exception
def get_shared_mindmaps():
"""Listet alle Mindmaps auf, die mit dem aktuellen Benutzer geteilt wurden."""
shares = MindmapShare.query.filter_by(shared_with_id=current_user.id).all()
result = []
for share in shares:
mindmap = UserMindmap.query.get(share.mindmap_id)
owner = User.query.get(mindmap.user_id)
if mindmap and owner:
# Zugriffsdatum aktualisieren
share.last_accessed = datetime.now(timezone.utc)
result.append({
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
'owner': {
'id': owner.id,
'username': owner.username
},
'created_at': mindmap.created_at.isoformat(),
'last_modified': mindmap.last_modified.isoformat(),
'permission': share.permission_type.name
})
db.session.commit() # Speichere die aktualisierten Zugriffsdaten
return jsonify({
'success': True,
'mindmaps': result
})
# Hilfsfunktion zur Überprüfung der Berechtigungen
def check_mindmap_permission(mindmap_id, permission_type=None):
"""
Überprüft, ob der aktuelle Benutzer Zugriff auf eine Mindmap hat.
Args:
mindmap_id: ID der Mindmap
permission_type: Mindestberechtigung, die erforderlich ist (READ, EDIT, ADMIN)
Wenn None, wird nur der Zugriff überprüft.
Returns:
(bool, str): Tupel aus (hat_berechtigung, fehlermeldung)
"""
mindmap = UserMindmap.query.get(mindmap_id)
if not mindmap:
return False, "Mindmap nicht gefunden."
# Wenn der Benutzer der Eigentümer ist, hat er vollen Zugriff
if mindmap.user_id == current_user.id:
return True, ""
# Wenn die Mindmap privat ist und der Benutzer nicht der Eigentümer ist
if mindmap.is_private:
share = MindmapShare.query.filter_by(
mindmap_id=mindmap_id,
shared_with_id=current_user.id
).first()
if not share:
return False, "Sie haben keinen Zugriff auf diese Mindmap."
# Wenn eine bestimmte Berechtigungsstufe erforderlich ist
if permission_type:
required_level = PermissionType[permission_type].value
user_level = share.permission_type.value
if required_level not in [p.value for p in PermissionType]:
return False, f"Ungültiger Berechtigungstyp: {permission_type}"
# Berechtigungshierarchie prüfen
permission_hierarchy = {
PermissionType.READ.name: 1,
PermissionType.EDIT.name: 2,
PermissionType.ADMIN.name: 3
}
if permission_hierarchy[share.permission_type.name] < permission_hierarchy[permission_type]:
return False, f"Sie benötigen {permission_type}-Berechtigungen für diese Aktion."
return True, ""
# Flask starten
if __name__ == '__main__':
with app.app_context():
@@ -2298,6 +2737,394 @@ def get_public_mindmap_nodes():
print(f"Fehler in get_public_mindmap_nodes: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
# Suchfunktion für Mindmap-Knoten
@app.route('/api/search/mindmap', methods=['GET'])
@handle_api_exception
def search_mindmap_nodes():
"""
Durchsucht Mindmap-Knoten nach einem Suchbegriff.
Query-Parameter:
- q: Suchbegriff
- user_only: Wenn "true", werden nur Knoten aus den Mindmaps des Benutzers durchsucht
- include_public: Wenn "true", werden auch öffentliche Knoten einbezogen (Standard: true)
"""
try:
# Parameter auslesen
query = request.args.get('q', '').strip()
user_only = request.args.get('user_only', 'false').lower() == 'true'
include_public = request.args.get('include_public', 'true').lower() == 'true'
if not query:
return jsonify({
'success': False,
'message': 'Kein Suchbegriff angegeben',
'results': []
}), 400
# Basisfunktion für die Suche
results = []
# Erstelle Such-Pattern (beide Suchbegriffe werden verwendet)
search_pattern = f"%{query}%"
# 1. Knoten aus Benutzer-Mindmaps suchen, falls Benutzer angemeldet ist
if current_user.is_authenticated:
# Suche in allen Knoten, die in den Mindmaps des Benutzers sind
user_nodes_query = db.session.query(
MindMapNode, UserMindmapNode
).join(
UserMindmapNode, UserMindmapNode.node_id == MindMapNode.id
).join(
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
).filter(
UserMindmap.user_id == current_user.id,
db.or_(
MindMapNode.name.ilike(search_pattern),
MindMapNode.description.ilike(search_pattern)
)
).all()
# Formatiere die Ergebnisse
for node, user_node in user_nodes_query:
# Hole den Mindmap-Namen für diesen Knoten
mindmap = UserMindmap.query.get(user_node.user_mindmap_id)
results.append({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA',
'source': 'user_mindmap',
'mindmap_id': user_node.user_mindmap_id,
'mindmap_name': mindmap.name if mindmap else 'Unbekannte Mindmap',
'position': {
'x': user_node.x_position,
'y': user_node.y_position
}
})
# 2. Öffentliche Knoten suchen, falls gewünscht und nicht nur Benutzer-Mindmaps
if include_public and not user_only:
public_nodes_query = MindMapNode.query.filter(
MindMapNode.is_public == True,
db.or_(
MindMapNode.name.ilike(search_pattern),
MindMapNode.description.ilike(search_pattern)
)
).all()
# Prüfen, ob die öffentlichen Knoten bereits in den Ergebnissen sind
existing_node_ids = [node['id'] for node in results]
for node in public_nodes_query:
if node.id not in existing_node_ids:
results.append({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA',
'source': 'public',
'mindmap_id': None,
'mindmap_name': 'Öffentliche Mindmap'
})
return jsonify({
'success': True,
'message': f'{len(results)} Ergebnisse gefunden',
'results': results
})
except Exception as e:
print(f"Fehler bei der Mindmap-Suche: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler bei der Suche: {str(e)}',
'results': []
}), 500
# Export/Import-Funktionen für Mindmaps
@app.route('/api/mindmap/<int:mindmap_id>/export', methods=['GET'])
@login_required
@handle_api_exception
def export_mindmap(mindmap_id):
"""
Exportiert eine Mindmap im angegebenen Format.
Query-Parameter:
- format: Format der Exportdatei (json, xml, csv)
"""
try:
# Sicherheitscheck: Nur eigene Mindmaps oder Mindmaps, auf die der Benutzer Zugriff hat
mindmap = UserMindmap.query.get_or_404(mindmap_id)
# Prüfen, ob der Benutzer Zugriff auf diese Mindmap hat
can_access = mindmap.user_id == current_user.id
if not can_access and mindmap.is_private:
return jsonify({
'success': False,
'message': 'Keine Berechtigung für den Zugriff auf diese Mindmap'
}), 403
# Format aus Query-Parameter holen
export_format = request.args.get('format', 'json')
# Alle Knoten und ihre Positionen in dieser Mindmap holen
nodes_data = db.session.query(
MindMapNode, UserMindmapNode
).join(
UserMindmapNode, UserMindmapNode.node_id == MindMapNode.id
).filter(
UserMindmapNode.user_mindmap_id == mindmap_id
).all()
# Beziehungen zwischen Knoten holen
relationships = []
for node1, user_node1 in nodes_data:
for node2, user_node2 in nodes_data:
if node1.id != node2.id and node2 in node1.children:
relationships.append({
'source': node1.id,
'target': node2.id
})
# Exportdaten vorbereiten
export_data = {
'mindmap': {
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
'created_at': mindmap.created_at.isoformat(),
'last_modified': mindmap.last_modified.isoformat()
},
'nodes': [{
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA',
'x_position': user_node.x_position,
'y_position': user_node.y_position,
'scale': user_node.scale or 1.0
} for node, user_node in nodes_data],
'relationships': relationships
}
# Exportieren im angeforderten Format
if export_format == 'json':
response = app.response_class(
response=json.dumps(export_data, indent=2),
status=200,
mimetype='application/json'
)
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}.json"
return response
elif export_format == 'xml':
import dicttoxml
xml_data = dicttoxml.dicttoxml(export_data)
response = app.response_class(
response=xml_data,
status=200,
mimetype='application/xml'
)
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}.xml"
return response
elif export_format == 'csv':
import io
import csv
# CSV kann nicht die gesamte Struktur darstellen, daher nur die Knotenliste
output = io.StringIO()
writer = csv.writer(output)
# Schreibe Header
writer.writerow(['id', 'name', 'description', 'color_code', 'x_position', 'y_position', 'scale'])
# Schreibe Knotendaten
for node, user_node in nodes_data:
writer.writerow([
node.id,
node.name,
node.description or '',
node.color_code or '#9F7AEA',
user_node.x_position,
user_node.y_position,
user_node.scale or 1.0
])
output.seek(0)
response = app.response_class(
response=output.getvalue(),
status=200,
mimetype='text/csv'
)
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}_nodes.csv"
return response
else:
return jsonify({
'success': False,
'message': f'Nicht unterstütztes Format: {export_format}'
}), 400
except Exception as e:
print(f"Fehler beim Exportieren der Mindmap: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Exportieren: {str(e)}'
}), 500
@app.route('/api/mindmap/<int:mindmap_id>/import', methods=['POST'])
@login_required
@handle_api_exception
def import_mindmap(mindmap_id):
"""
Importiert Daten in eine bestehende Mindmap.
Die Daten können in verschiedenen Formaten (JSON, XML, CSV) hochgeladen werden.
"""
try:
# Sicherheitscheck: Nur eigene Mindmaps können bearbeitet werden
mindmap = UserMindmap.query.get_or_404(mindmap_id)
if mindmap.user_id != current_user.id:
return jsonify({
'success': False,
'message': 'Keine Berechtigung zum Bearbeiten dieser Mindmap'
}), 403
# Prüfen, ob eine Datei hochgeladen wurde
if 'file' not in request.files:
return jsonify({
'success': False,
'message': 'Keine Datei ausgewählt'
}), 400
file = request.files['file']
if file.filename == '':
return jsonify({
'success': False,
'message': 'Keine Datei ausgewählt'
}), 400
# Format anhand der Dateiendung erkennen
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else None
if file_ext not in ['json', 'xml', 'csv']:
return jsonify({
'success': False,
'message': f'Nicht unterstütztes Dateiformat: {file_ext}'
}), 400
# Datei einlesen
import_data = None
if file_ext == 'json':
import_data = json.loads(file.read().decode('utf-8'))
elif file_ext == 'xml':
import xml.etree.ElementTree as ET
import xmltodict
xml_data = file.read().decode('utf-8')
import_data = xmltodict.parse(xml_data)
elif file_ext == 'csv':
import io
import csv
csv_data = file.read().decode('utf-8')
reader = csv.DictReader(io.StringIO(csv_data))
nodes = []
for row in reader:
nodes.append(row)
import_data = {'nodes': nodes}
# Daten in die Mindmap importieren
if 'nodes' in import_data:
# Bestehende Knoten in der Mindmap für Referenz
existing_nodes = db.session.query(
UserMindmapNode.node_id
).filter_by(
user_mindmap_id=mindmap_id
).all()
existing_node_ids = [n[0] for n in existing_nodes]
# Mapping von alten zu neuen Knoten-IDs für importierte Knoten
id_mapping = {}
# Knoten importieren
for node_data in import_data.get('nodes', []):
# Prüfen, ob es sich um Stringkeys (aus CSV) oder Dict (aus JSON) handelt
if isinstance(node_data, dict):
node_name = node_data.get('name')
node_desc = node_data.get('description', '')
node_color = node_data.get('color_code', '#9F7AEA')
x_pos = float(node_data.get('x_position', 0))
y_pos = float(node_data.get('y_position', 0))
node_scale = float(node_data.get('scale', 1.0))
old_id = node_data.get('id')
else:
# Fallback für andere Formate
continue
# Neuen Knoten erstellen, wenn nötig
new_node = MindMapNode(
name=node_name,
description=node_desc,
color_code=node_color
)
db.session.add(new_node)
db.session.flush() # ID generieren
# Verknüpfung zur Mindmap erstellen
user_node = UserMindmapNode(
user_mindmap_id=mindmap_id,
node_id=new_node.id,
x_position=x_pos,
y_position=y_pos,
scale=node_scale
)
db.session.add(user_node)
# ID-Mapping für Beziehungen speichern
if old_id:
id_mapping[old_id] = new_node.id
# Beziehungen zwischen Knoten importieren
for rel in import_data.get('relationships', []):
source_id = rel.get('source')
target_id = rel.get('target')
if source_id in id_mapping and target_id in id_mapping:
# Knoten-Objekte holen
source_node = MindMapNode.query.get(id_mapping[source_id])
target_node = MindMapNode.query.get(id_mapping[target_id])
if source_node and target_node:
# Beziehung erstellen
source_node.children.append(target_node)
db.session.commit()
return jsonify({
'success': True,
'message': f'{len(import_data.get("nodes", []))} Knoten erfolgreich importiert'
})
else:
return jsonify({
'success': False,
'message': 'Keine Knotendaten in der Importdatei gefunden'
}), 400
except Exception as e:
db.session.rollback()
print(f"Fehler beim Importieren der Mindmap: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Importieren: {str(e)}'
}), 500
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
def initialize_app():
"""Initialisierung der Anwendung"""

Binary file not shown.

27
logs/app.log Normal file
View File

@@ -0,0 +1,27 @@
2025-05-10 23:12:44,110 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
2025-05-10 23:12:45,854 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
2025-05-10 23:12:45,854 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
2025-05-10 23:13:27,379 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
2025-05-10 23:13:29,289 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
2025-05-10 23:13:29,289 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
2025-05-10 23:13:35,686 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:13:37,640 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:13:37,640 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:14:35,907 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:14:37,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:14:37,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:14:44,251 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:14:46,088 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:14:46,088 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:15:14,106 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:15:15,855 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:15:15,855 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
2025-05-10 23:15:30,739 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:15:32,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:15:32,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:16:55,581 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:16:57,283 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:16:57,283 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:17:04,727 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:17:06,698 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
2025-05-10 23:17:06,698 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]

View File

@@ -359,4 +359,33 @@ class ForumPost(db.Model):
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
def __repr__(self):
return f'<ForumPost {self.title}>'
return f'<ForumPost {self.title}>'
# Berechtigungstypen für Mindmap-Freigaben
class PermissionType(Enum):
READ = "Nur-Lesen"
EDIT = "Bearbeiten"
ADMIN = "Administrator"
# Freigabemodell für Mindmaps
class MindmapShare(db.Model):
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
id = db.Column(db.Integer, primary_key=True)
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_accessed = db.Column(db.DateTime, nullable=True)
# Beziehungen
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
__table_args__ = (
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
)
def __repr__(self):
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'

38
templates/errors/400.html Normal file
View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}400 - Ungültige Anfrage{% endblock %}
{% block content %}
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
<div class="text-center">
<div class="flex justify-center mb-6">
<div class="relative">
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">400</h1>
<div class="absolute -top-4 -right-4 w-12 h-12 bg-red-500 rounded-full flex items-center justify-center animate-pulse">
<i class="fa-solid fa-exclamation text-white text-xl"></i>
</div>
</div>
</div>
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Ungültige Anfrage</h2>
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Die Anfrage konnte nicht verarbeitet werden. Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut.</p>
{% if error %}
<div class="mb-6 p-4 bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 rounded-lg text-sm">
{{ error }}
</div>
{% endif %}
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
</a>
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
</a>
</div>
</div>
</div>
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
</div>
</div>
{% endblock %}

View File

@@ -229,6 +229,87 @@
gap: 0.5rem;
}
/* Suchergebnisse Container */
.search-results-container {
position: absolute;
top: 5rem;
left: 1rem;
width: 350px;
max-height: calc(100vh - 16rem);
overflow-y: auto;
border-radius: 1rem;
opacity: 0;
transform: translateY(-10px);
pointer-events: none;
transition: all 0.3s ease;
z-index: 100;
}
.search-results-container.visible {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
body.dark .search-results-container {
background-color: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
body:not(.dark) .search-results-container {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
color: #1e293b;
}
/* Suchergebnis-Item */
.search-result-item {
padding: 0.75rem;
border-bottom: 1px solid rgba(127, 127, 127, 0.2);
cursor: pointer;
transition: all 0.2s ease;
}
.search-result-item:last-child {
border-bottom: none;
}
body.dark .search-result-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) .search-result-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.search-result-title {
font-weight: 600;
margin-bottom: 0.25rem;
display: flex;
align-items: center;
}
.search-result-color {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
margin-right: 0.5rem;
display: inline-block;
}
.search-result-desc {
font-size: 0.85rem;
opacity: 0.8;
}
.search-result-source {
font-size: 0.75rem;
opacity: 0.6;
margin-top: 0.25rem;
}
body.dark .menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
@@ -299,6 +380,16 @@
<!-- Toolbar -->
<div class="mindmap-toolbar mb-4">
<!-- Suchleiste -->
<div class="search-container mr-2">
<div class="relative">
<input type="text" id="mindmap-search" placeholder="Knoten suchen..."
class="px-3 py-2 pr-10 rounded-lg text-sm border dark:bg-gray-700 dark:text-white dark:border-gray-600">
<button id="search-btn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<button id="fit-btn" class="mindmap-action-btn">
<i class="fas fa-expand"></i>
<span>Alles anzeigen</span>
@@ -319,6 +410,20 @@
<i class="fas fa-save"></i>
<span>Layout speichern</span>
</button>
{% if mindmap.user_id == current_user.id %}
<button id="share-btn" class="mindmap-action-btn">
<i class="fas fa-share-alt"></i>
<span>Freigeben</span>
</button>
{% endif %}
<button id="export-btn" class="mindmap-action-btn">
<i class="fas fa-file-export"></i>
<span>Exportieren</span>
</button>
<button id="import-btn" class="mindmap-action-btn">
<i class="fas fa-file-import"></i>
<span>Importieren</span>
</button>
<button id="toggle-public-mindmap-btn" class="mindmap-action-btn">
<i class="fas fa-globe"></i>
<span>Öffentliche Mindmap</span>
@@ -336,6 +441,19 @@
<div id="public-cy" style="width: 100%; height: 250px;"></div>
</div>
<!-- Suchergebnisse Container -->
<div id="search-results-container" class="search-results-container p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="text-lg font-semibold">Suchergebnisse</h3>
<button id="close-search" class="text-sm opacity-70 hover:opacity-100">
<i class="fas fa-times"></i>
</button>
</div>
<div id="search-results-list" class="space-y-1">
<!-- Suchergebnisse werden hier dynamisch hinzugefügt -->
</div>
</div>
<!-- Informationsanzeige für ausgewählten Knoten -->
<div id="node-info-panel" class="node-info-panel p-4">
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
@@ -952,6 +1070,505 @@
});
}
// Suchfunktionalität implementieren
const searchInput = document.getElementById('mindmap-search');
const searchBtn = document.getElementById('search-btn');
const searchResultsContainer = document.getElementById('search-results-container');
const searchResultsList = document.getElementById('search-results-list');
const closeSearchBtn = document.getElementById('close-search');
// Funktion zum Schließen der Suchergebnisse
function closeSearchResults() {
searchResultsContainer.classList.remove('visible');
searchInput.value = '';
}
// Event-Listener für die Suche
searchBtn.addEventListener('click', performSearch);
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
closeSearchBtn.addEventListener('click', closeSearchResults);
// Funktion für die Suche
function performSearch() {
const query = searchInput.value.trim();
if (!query) {
showUINotification('Bitte geben Sie einen Suchbegriff ein.', 'info');
return;
}
// Lade-Animation im Suchergebnisbereich anzeigen
searchResultsList.innerHTML = '<div class="p-4 text-center"><i class="fas fa-spinner fa-spin mr-2"></i> Suche läuft...</div>';
searchResultsContainer.classList.add('visible');
// API-Anfrage für die Suche
fetch(`/api/search/mindmap?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displaySearchResults(data.results, query);
} else {
searchResultsList.innerHTML = `<div class="p-4 text-center text-red-500">Fehler: ${data.message}</div>`;
}
})
.catch(error => {
console.error('Fehler bei der Suche:', error);
searchResultsList.innerHTML = '<div class="p-4 text-center text-red-500">Ein Fehler ist aufgetreten.</div>';
});
}
// Funktion zum Anzeigen der Suchergebnisse
function displaySearchResults(results, query) {
searchResultsList.innerHTML = '';
if (results.length === 0) {
searchResultsList.innerHTML = '<div class="p-4 text-center">Keine Ergebnisse gefunden.</div>';
return;
}
// Hervorheben-Funktion für den Suchbegriff
function highlightText(text, query) {
if (!text) return '';
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(regex, '<span class="bg-yellow-200 dark:bg-yellow-800">$1</span>');
}
// Ergebnisse anzeigen
results.forEach(result => {
const item = document.createElement('div');
item.className = 'search-result-item';
// Inhalte mit hervorgehobenem Suchbegriff
const highlightedName = highlightText(result.name, query);
const highlightedDesc = highlightText(result.description, query);
item.innerHTML = `
<div class="search-result-title">
<span class="search-result-color" style="background-color: ${result.color_code}"></span>
<span>${highlightedName}</span>
</div>
<div class="search-result-desc">${highlightedDesc}</div>
<div class="search-result-source">
<i class="fas fa-map-marked-alt mr-1"></i> ${result.mindmap_name}
</div>
`;
// Event-Listener zum Springen zum Knoten
item.addEventListener('click', () => {
if (result.source === 'user_mindmap') {
// Knoten ist bereits in der aktuellen Mindmap
const node = cy.$id(result.id);
if (node.length > 0) {
// Knoten auswählen und zentrieren
node.select();
cy.animate({
center: { eles: node },
zoom: 1.5,
duration: 500
});
// Aktualisiere das Info-Panel
nodeDescription.textContent = result.description || 'Keine Beschreibung für diesen Knoten.';
nodeInfoPanel.classList.add('visible');
// Suchergebnisse schließen
closeSearchResults();
} else {
// Knoten ist in einer anderen Mindmap des Benutzers
if (result.mindmap_id && result.mindmap_id !== parseInt("{{ mindmap.id }}")) {
if (confirm(`Dieser Knoten befindet sich in der Mindmap "${result.mindmap_name}". Möchten Sie zu dieser Mindmap wechseln?`)) {
window.location.href = `/my-mindmap/${result.mindmap_id}`;
}
} else {
showUINotification('Knoten konnte nicht gefunden werden.', 'error');
}
}
} else if (result.source === 'public') {
// Knoten ist in der öffentlichen Mindmap, frage ob er hinzugefügt werden soll
if (confirm(`Möchten Sie den Knoten "${result.name}" aus der öffentlichen Mindmap zu Ihrer Mindmap hinzufügen?`)) {
// Dialog zum Hinzufügen des Knotens anzeigen
showAddNodeDialog(result.id, result.name, result.description);
closeSearchResults();
}
}
});
searchResultsList.appendChild(item);
});
}
// Export-Funktionalität implementieren
document.getElementById('export-btn').addEventListener('click', function() {
const mindmapId = parseInt("{{ mindmap.id }}");
// Dialog für Export-Format anzeigen
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
overlay.id = 'export-dialog-overlay';
overlay.innerHTML = `
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 class="text-xl font-bold mb-4">Mindmap exportieren</h3>
<div class="mb-4">
<label class="block mb-2">Format auswählen:</label>
<select id="export-format" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="json">JSON</option>
<option value="xml">XML</option>
<option value="csv">CSV</option>
</select>
</div>
<div class="flex justify-end gap-2">
<button id="cancel-export" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
<button id="confirm-export" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Exportieren</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Event-Listener für Export-Buttons
document.getElementById('cancel-export').addEventListener('click', function() {
document.getElementById('export-dialog-overlay').remove();
});
document.getElementById('confirm-export').addEventListener('click', function() {
const format = document.getElementById('export-format').value;
// API-Anfrage für den Export
fetch(`/api/mindmap/${mindmapId}/export?format=${format}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then(blob => {
// Download der Export-Datei
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `mindmap_${mindmapId}.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.getElementById('export-dialog-overlay').remove();
showUINotification(`Mindmap wurde erfolgreich als ${format.toUpperCase()} exportiert.`, 'success');
})
.catch(error => {
console.error('Fehler beim Exportieren der Mindmap:', error);
showUINotification('Fehler beim Exportieren der Mindmap.', 'error');
document.getElementById('export-dialog-overlay').remove();
});
});
});
// Import-Funktionalität implementieren
document.getElementById('import-btn').addEventListener('click', function() {
const mindmapId = parseInt("{{ mindmap.id }}");
// Dialog für Import-Format anzeigen
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
overlay.id = 'import-dialog-overlay';
overlay.innerHTML = `
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 class="text-xl font-bold mb-4">Mindmap importieren</h3>
<div class="mb-4">
<label class="block mb-2">Datei auswählen:</label>
<input type="file" id="import-file" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
accept=".json,.xml,.csv">
</div>
<div class="flex justify-end gap-2">
<button id="cancel-import" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
<button id="confirm-import" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Importieren</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Event-Listener für Import-Buttons
document.getElementById('cancel-import').addEventListener('click', function() {
document.getElementById('import-dialog-overlay').remove();
});
document.getElementById('confirm-import').addEventListener('click', function() {
const fileInput = document.getElementById('import-file');
if (!fileInput.files || fileInput.files.length === 0) {
showUINotification('Bitte wählen Sie eine Datei aus.', 'error');
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
// API-Anfrage für den Import
fetch(`/api/mindmap/${mindmapId}/import`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('import-dialog-overlay').remove();
showUINotification('Mindmap wurde erfolgreich importiert.', 'success');
// Seite nach kurzer Verzögerung neu laden, um die importierten Daten anzuzeigen
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showUINotification(`Fehler beim Importieren: ${data.message}`, 'error');
}
})
.catch(error => {
console.error('Fehler beim Importieren der Mindmap:', error);
showUINotification('Fehler beim Importieren der Mindmap.', 'error');
});
});
});
// Freigabe-Funktionalität implementieren
if (document.getElementById('share-btn')) {
document.getElementById('share-btn').addEventListener('click', function() {
const mindmapId = parseInt("{{ mindmap.id }}");
// Dialog für Mindmap-Freigabe anzeigen
showShareDialog(mindmapId);
});
}
// Funktion zum Anzeigen des Freigabe-Dialogs
function showShareDialog(mindmapId) {
// Dialog erstellen
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
overlay.id = 'share-dialog-overlay';
overlay.innerHTML = `
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-lg w-full mx-4">
<h3 class="text-xl font-bold mb-4">Mindmap freigeben</h3>
<div class="mb-6">
<div class="flex flex-col space-y-4">
<div>
<label class="block mb-2 text-sm font-medium">E-Mail-Adresse des Benutzers:</label>
<input type="email" id="share-email" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="beispiel@mail.com">
</div>
<div>
<label class="block mb-2 text-sm font-medium">Berechtigungen:</label>
<select id="share-permission" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="READ">Nur-Lesen</option>
<option value="EDIT">Bearbeiten</option>
<option value="ADMIN">Administrator</option>
</select>
</div>
<button id="add-share-btn" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">
<i class="fas fa-plus mr-1"></i> Hinzufügen
</button>
</div>
</div>
<div class="mb-4">
<h4 class="font-semibold mb-2">Aktuelle Freigaben</h4>
<div id="share-list" class="max-h-60 overflow-y-auto border rounded-lg p-2 dark:border-gray-700">
<div class="text-center text-sm opacity-70 p-4">Lade Freigaben...</div>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button id="close-share-dialog" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Schließen</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Aktuelle Freigaben laden
loadShares(mindmapId);
// Event-Listener für Buttons
document.getElementById('close-share-dialog').addEventListener('click', function() {
document.getElementById('share-dialog-overlay').remove();
});
document.getElementById('add-share-btn').addEventListener('click', function() {
const email = document.getElementById('share-email').value.trim();
const permission = document.getElementById('share-permission').value;
if (!email) {
showUINotification('Bitte geben Sie eine E-Mail-Adresse ein.', 'error');
return;
}
// Mindmap freigeben
fetch(`/api/mindmap/${mindmapId}/share`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
email: email,
permission: permission
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showUINotification(data.message, 'success');
document.getElementById('share-email').value = '';
loadShares(mindmapId); // Freigaben neu laden
} else {
showUINotification(data.error, 'error');
}
})
.catch(error => {
console.error('Fehler beim Freigeben der Mindmap:', error);
showUINotification('Fehler beim Freigeben der Mindmap.', 'error');
});
});
}
// Funktion zum Laden der aktuellen Freigaben
function loadShares(mindmapId) {
const shareList = document.getElementById('share-list');
if (!shareList) return;
shareList.innerHTML = '<div class="text-center text-sm opacity-70 p-4">Lade Freigaben...</div>';
fetch(`/api/mindmap/${mindmapId}/shares`)
.then(response => response.json())
.then(data => {
if (data.success) {
const shares = data.shares;
if (shares.length === 0) {
shareList.innerHTML = '<div class="text-center text-sm opacity-70 p-4">Keine Freigaben vorhanden.</div>';
return;
}
shareList.innerHTML = '';
shares.forEach(share => {
const shareItem = document.createElement('div');
shareItem.className = 'flex justify-between items-center p-2 border-b dark:border-gray-700 last:border-0';
const permissionClass = {
'READ': 'text-blue-500 dark:text-blue-400',
'EDIT': 'text-green-500 dark:text-green-400',
'ADMIN': 'text-purple-500 dark:text-purple-400'
}[share.permission] || 'text-gray-500';
shareItem.innerHTML = `
<div>
<div class="font-medium">${share.username}</div>
<div class="text-xs opacity-70">${share.email}</div>
</div>
<div class="flex items-center space-x-2">
<select class="share-permission-select text-sm p-1 rounded ${permissionClass}" data-share-id="${share.id}">
<option value="READ" ${share.permission === 'READ' ? 'selected' : ''}>Nur-Lesen</option>
<option value="EDIT" ${share.permission === 'EDIT' ? 'selected' : ''}>Bearbeiten</option>
<option value="ADMIN" ${share.permission === 'ADMIN' ? 'selected' : ''}>Administrator</option>
</select>
<button class="text-red-500 hover:text-red-700" data-share-id="${share.id}">
<i class="fas fa-times"></i>
</button>
</div>
`;
// Event-Listener zum Aktualisieren der Berechtigungen
const permissionSelect = shareItem.querySelector('.share-permission-select');
permissionSelect.addEventListener('change', function() {
const shareId = this.dataset.shareId;
const newPermission = this.value;
updateSharePermission(shareId, newPermission);
});
// Event-Listener zum Löschen der Freigabe
const deleteButton = shareItem.querySelector('button[data-share-id]');
deleteButton.addEventListener('click', function() {
const shareId = this.dataset.shareId;
if (confirm('Möchten Sie diese Freigabe wirklich widerrufen?')) {
revokeShare(shareId, mindmapId);
}
});
shareList.appendChild(shareItem);
});
} else {
shareList.innerHTML = '<div class="text-center text-sm text-red-500 p-4">Fehler beim Laden der Freigaben.</div>';
}
})
.catch(error => {
console.error('Fehler beim Laden der Freigaben:', error);
shareList.innerHTML = '<div class="text-center text-sm text-red-500 p-4">Fehler beim Laden der Freigaben.</div>';
});
}
// Funktion zum Aktualisieren der Berechtigungen einer Freigabe
function updateSharePermission(shareId, permission) {
fetch(`/api/mindmap/shares/${shareId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
permission: permission
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showUINotification(data.message, 'success');
} else {
showUINotification(data.error, 'error');
}
})
.catch(error => {
console.error('Fehler beim Aktualisieren der Berechtigungen:', error);
showUINotification('Fehler beim Aktualisieren der Berechtigungen.', 'error');
});
}
// Funktion zum Widerrufen einer Freigabe
function revokeShare(shareId, mindmapId) {
fetch(`/api/mindmap/shares/${shareId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showUINotification(data.message, 'success');
loadShares(mindmapId); // Freigaben neu laden
} else {
showUINotification(data.error, 'error');
}
})
.catch(error => {
console.error('Fehler beim Widerrufen der Freigabe:', error);
showUINotification('Fehler beim Widerrufen der Freigabe.', 'error');
});
}
} else {
// Fallback, falls mindmapData null ist
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";