first commit

This commit is contained in:
dgsoft
2025-10-14 21:27:41 +02:00
commit 44b8667f31
40 changed files with 4619 additions and 0 deletions

59
app/__init__.py Normal file
View File

@@ -0,0 +1,59 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_cors import CORS
import os
from dotenv import load_dotenv
# Lade Umgebungsvariablen
load_dotenv()
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
# Konfiguration
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'dev-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or 'sqlite:///querybuilder.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Initialisiere Extensions
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Bitte melden Sie sich an, um auf diese Seite zuzugreifen.'
CORS(app)
# Registriere Blueprints
from app.routes.auth import auth_bp
from app.routes.main import main_bp
from app.routes.api import api_bp
from app.routes.admin import admin_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
app.register_blueprint(api_bp, url_prefix='/api')
app.register_blueprint(admin_bp, url_prefix='/admin')
# Erstelle Datenbanktabellen
with app.app_context():
db.create_all()
# Erstelle Standard-Admin-User falls nicht vorhanden
from app.models import User
admin = User.query.filter_by(username='admin').first()
if not admin:
admin = User(username='admin', email='admin@example.com')
admin.set_password('admin123')
db.session.add(admin)
db.session.commit()
return app
@login_manager.user_loader
def load_user(user_id):
from app.models import User
return User.query.get(int(user_id))

Binary file not shown.

Binary file not shown.

73
app/models.py Normal file
View File

@@ -0,0 +1,73 @@
from app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
import json
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Beziehungen
saved_queries = db.relationship('SavedQuery', backref='user', lazy=True, cascade='all, delete-orphan')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
class SavedQuery(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
query_text = db.Column('query', db.Text, nullable=False) # Expliziter Spaltenname 'query', Attribut 'query_text'
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'query': self.query_text,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat()
}
def __repr__(self):
return f'<SavedQuery {self.name}>'
class DatabaseConnection(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
host = db.Column(db.String(200))
port = db.Column(db.Integer)
database = db.Column(db.String(100))
username = db.Column(db.String(100))
password = db.Column(db.String(200)) # In Produktion sollte dies verschlüsselt werden
db_type = db.Column(db.String(50)) # mysql, postgresql, sqlite, etc.
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'host': self.host,
'port': self.port,
'database': self.database,
'username': self.username,
'db_type': self.db_type
}
def __repr__(self):
return f'<DatabaseConnection {self.name}>'

Binary file not shown.

Binary file not shown.

Binary file not shown.

187
app/routes/admin.py Normal file
View File

@@ -0,0 +1,187 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from functools import wraps
from app.models import User, db
from werkzeug.security import generate_password_hash
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Zugriff verweigert. Administrator-Rechte erforderlich.', 'error')
return redirect(url_for('main.dashboard'))
return f(*args, **kwargs)
return decorated_function
@admin_bp.route('/')
@admin_required
def admin_dashboard():
"""Admin Dashboard mit Benutzerübersicht"""
users = User.query.order_by(User.created_at.desc()).all()
return render_template('admin/dashboard.html', users=users)
@admin_bp.route('/users')
@admin_required
def users():
"""Benutzerverwaltung"""
users = User.query.order_by(User.created_at.desc()).all()
return render_template('admin/users.html', users=users)
@admin_bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Neuen Benutzer erstellen"""
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '').strip()
is_admin = 'is_admin' in request.form
# Validierung
if not username or not email or not password:
flash('Alle Felder sind erforderlich.', 'error')
return render_template('admin/create_user.html')
# Prüfe ob Benutzername bereits existiert
if User.query.filter_by(username=username).first():
flash('Benutzername bereits vergeben.', 'error')
return render_template('admin/create_user.html')
# Prüfe ob E-Mail bereits existiert
if User.query.filter_by(email=email).first():
flash('E-Mail-Adresse bereits vergeben.', 'error')
return render_template('admin/create_user.html')
try:
# Benutzer erstellen
user = User(
username=username,
email=email,
is_admin=is_admin
)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash(f'Benutzer "{username}" erfolgreich erstellt.', 'success')
return redirect(url_for('admin.users'))
except Exception as e:
db.session.rollback()
flash(f'Fehler beim Erstellen des Benutzers: {str(e)}', 'error')
return render_template('admin/create_user.html')
@admin_bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
@admin_required
def edit_user(user_id):
"""Benutzer bearbeiten"""
user = User.query.get_or_404(user_id)
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '').strip()
is_admin = 'is_admin' in request.form
# Validierung
if not username or not email:
flash('Benutzername und E-Mail sind erforderlich.', 'error')
return render_template('admin/edit_user.html', user=user)
# Prüfe ob Benutzername bereits von anderem User verwendet wird
existing_user = User.query.filter_by(username=username).first()
if existing_user and existing_user.id != user_id:
flash('Benutzername bereits vergeben.', 'error')
return render_template('admin/edit_user.html', user=user)
# Prüfe ob E-Mail bereits von anderem User verwendet wird
existing_email = User.query.filter_by(email=email).first()
if existing_email and existing_email.id != user_id:
flash('E-Mail-Adresse bereits vergeben.', 'error')
return render_template('admin/edit_user.html', user=user)
try:
# Benutzer aktualisieren
user.username = username
user.email = email
user.is_admin = is_admin
# Passwort nur ändern wenn angegeben
if password:
user.set_password(password)
db.session.commit()
flash(f'Benutzer "{username}" erfolgreich aktualisiert.', 'success')
return redirect(url_for('admin.users'))
except Exception as e:
db.session.rollback()
flash(f'Fehler beim Aktualisieren des Benutzers: {str(e)}', 'error')
return render_template('admin/edit_user.html', user=user)
@admin_bp.route('/users/<int:user_id>/delete', methods=['POST'])
@admin_required
def delete_user(user_id):
"""Benutzer löschen"""
user = User.query.get_or_404(user_id)
# Verhindere dass der Admin sich selbst löscht
if user.id == current_user.id:
flash('Sie können sich nicht selbst löschen.', 'error')
return redirect(url_for('admin.users'))
# Verhindere das Löschen des letzten Admins
if user.is_admin:
admin_count = User.query.filter_by(is_admin=True).count()
if admin_count <= 1:
flash('Der letzte Administrator kann nicht gelöscht werden.', 'error')
return redirect(url_for('admin.users'))
try:
username = user.username
db.session.delete(user)
db.session.commit()
flash(f'Benutzer "{username}" erfolgreich gelöscht.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Fehler beim Löschen des Benutzers: {str(e)}', 'error')
return redirect(url_for('admin.users'))
@admin_bp.route('/users/<int:user_id>/toggle_admin', methods=['POST'])
@admin_required
def toggle_admin(user_id):
"""Admin-Status umschalten"""
user = User.query.get_or_404(user_id)
# Verhindere dass der Admin sich selbst die Admin-Rechte entzieht
if user.id == current_user.id and user.is_admin:
return jsonify({'error': 'Sie können sich nicht selbst die Admin-Rechte entziehen.'}), 400
# Verhindere das Entziehen der Admin-Rechte des letzten Admins
if user.is_admin:
admin_count = User.query.filter_by(is_admin=True).count()
if admin_count <= 1:
return jsonify({'error': 'Der letzte Administrator kann nicht degradiert werden.'}), 400
try:
user.is_admin = not user.is_admin
db.session.commit()
status = 'Administrator' if user.is_admin else 'Benutzer'
return jsonify({
'success': True,
'message': f'{user.username} ist jetzt {status}.',
'is_admin': user.is_admin
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500

186
app/routes/api.py Normal file
View File

@@ -0,0 +1,186 @@
from flask import Blueprint, jsonify, request, make_response
from flask_login import login_required, current_user
from app.models import SavedQuery, db
from app.services.database_service import DatabaseService
from app.services.database_manager import DatabaseManager
import csv
import io
import json
api_bp = Blueprint('api', __name__)
@api_bp.route('/queries', methods=['GET'])
@login_required
def get_saved_queries():
"""API-Endpunkt um alle gespeicherten Queries zu erhalten"""
try:
queries = SavedQuery.query.filter_by(user_id=current_user.id).all()
return jsonify({
'success': True,
'queries': [query.to_dict() for query in queries]
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>', methods=['GET'])
@login_required
def get_query_by_name(query_name):
"""API-Endpunkt um eine gespeicherte Query nach Namen zu erhalten"""
try:
query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not query:
return jsonify({'error': 'Query nicht gefunden'}), 404
return jsonify({
'success': True,
'query': query.to_dict()
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>/execute', methods=['GET'])
@login_required
def execute_saved_query(query_name):
"""API-Endpunkt um eine gespeicherte Query auszuführen und Ergebnisse zu erhalten"""
try:
# Finde gespeicherte Query
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not saved_query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Datenbankverbindung bestimmen
connection = request.args.get('connection', 'oracle')
# Führe Query aus
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(saved_query.query_text)
# Format bestimmen (JSON oder CSV)
format_type = request.args.get('format', 'json').lower()
if format_type == 'csv':
return export_results_as_csv(results, query_name)
else:
return jsonify({
'success': True,
'query_name': query_name,
'results': results
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>/export/csv', methods=['GET'])
@login_required
def export_query_results_csv(query_name):
"""Exportiere Query-Ergebnisse als CSV"""
try:
# Finde gespeicherte Query
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not saved_query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Datenbankverbindung bestimmen
connection = request.args.get('connection', 'oracle')
# Führe Query aus
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(saved_query.query_text)
return export_results_as_csv(results, query_name)
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/queries/<query_name>/export/json', methods=['GET'])
@login_required
def export_query_results_json(query_name):
"""Exportiere Query-Ergebnisse als JSON"""
try:
# Finde gespeicherte Query
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
if not saved_query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Datenbankverbindung bestimmen
connection = request.args.get('connection', 'oracle')
# Führe Query aus
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(saved_query.query_text)
# Erstelle JSON-Response mit Download-Header
response = make_response(json.dumps({
'query_name': query_name,
'results': results
}, indent=2))
response.headers['Content-Type'] = 'application/json'
response.headers['Content-Disposition'] = f'attachment; filename="{query_name}_results.json"'
return response
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/update_query_name/<int:query_id>', methods=['PUT'])
@login_required
def update_query_name(query_id):
"""API-Endpunkt um den Namen einer gespeicherten Query zu aktualisieren"""
try:
data = request.get_json()
new_name = data.get('name', '').strip()
if not new_name:
return jsonify({'error': 'Name ist erforderlich'}), 400
# Finde Query
query = SavedQuery.query.filter_by(id=query_id, user_id=current_user.id).first()
if not query:
return jsonify({'error': 'Query nicht gefunden'}), 404
# Prüfe ob Name bereits existiert
existing_query = SavedQuery.query.filter_by(name=new_name, user_id=current_user.id).first()
if existing_query and existing_query.id != query_id:
return jsonify({'error': 'Eine Query mit diesem Namen existiert bereits'}), 400
# Update Name
query.name = new_name
db.session.commit()
return jsonify({
'success': True,
'message': 'Query-Name erfolgreich aktualisiert',
'query': query.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
def export_results_as_csv(results, filename):
"""Hilfsfunktion um Ergebnisse als CSV zu exportieren"""
if not results or 'data' not in results or not results['data']:
return jsonify({'error': 'Keine Daten zum Exportieren'}), 400
# Erstelle CSV
output = io.StringIO()
# Headers schreiben
if results['columns']:
writer = csv.writer(output)
writer.writerow(results['columns'])
# Daten schreiben
for row in results['data']:
writer.writerow(row)
# Response erstellen
response = make_response(output.getvalue())
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = f'attachment; filename="{filename}_results.csv"'
return response

56
app/routes/auth.py Normal file
View File

@@ -0,0 +1,56 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.models import User
from app import db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('main.dashboard'))
else:
flash('Ungültiger Benutzername oder Passwort')
return render_template('auth/login.html')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
email = request.form['email']
password = request.form['password']
# Prüfe ob Benutzer bereits existiert
if User.query.filter_by(username=username).first():
flash('Benutzername bereits vergeben')
return render_template('auth/register.html')
if User.query.filter_by(email=email).first():
flash('E-Mail bereits vergeben')
return render_template('auth/register.html')
# Erstelle neuen Benutzer
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registrierung erfolgreich')
return redirect(url_for('auth.login'))
return render_template('auth/register.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))

182
app/routes/main.py Normal file
View File

@@ -0,0 +1,182 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from app.models import SavedQuery, DatabaseConnection
from app.services.database_service import DatabaseService
from app.services.database_manager import DatabaseManager
from app import db
import traceback
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
@login_required
def dashboard():
# Lade gespeicherte Queries des Benutzers
saved_queries = SavedQuery.query.filter_by(user_id=current_user.id).all()
# Lade verfügbare Datenbankverbindungen
db_manager = DatabaseManager()
available_connections = db_manager.get_available_connections()
return render_template('dashboard.html',
saved_queries=saved_queries,
available_connections=available_connections)
@main_bp.route('/execute_query', methods=['POST'])
@login_required
def execute_query():
data = request.get_json()
query = data.get('query', '').strip()
connection = data.get('connection', 'oracle') # Standard: Oracle
if not query:
return jsonify({'error': 'Query ist leer'}), 400
try:
# Verwende ausgewählte Datenbankverbindung
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
results = db_service.execute_query(query)
return jsonify({
'success': True,
'results': results,
'connection': connection
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Ausführen der Query: {str(e)}',
'traceback': traceback.format_exc()
}), 500
@main_bp.route('/get_tables')
@login_required
def get_tables():
connection = request.args.get('connection', 'oracle')
try:
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
tables = db_service.get_tables()
return jsonify({
'success': True,
'tables': tables,
'connection': connection
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Laden der Tabellen: {str(e)}'
}), 500
@main_bp.route('/get_table_schema/<table_name>')
@login_required
def get_table_schema(table_name):
connection = request.args.get('connection', 'oracle')
try:
db_manager = DatabaseManager()
db_service = db_manager.get_database_service(connection)
schema = db_service.get_table_schema(table_name)
return jsonify({
'success': True,
'schema': schema,
'connection': connection
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Laden des Tabellenschemas: {str(e)}'
}), 500
@main_bp.route('/save_query', methods=['POST'])
@login_required
def save_query():
data = request.get_json()
name = data.get('name', '').strip()
description = data.get('description', '').strip()
query = data.get('query', '').strip()
if not name or not query:
return jsonify({'error': 'Name und Query sind erforderlich'}), 400
# Prüfe ob Query-Name bereits existiert
existing = SavedQuery.query.filter_by(name=name, user_id=current_user.id).first()
if existing:
return jsonify({'error': 'Query-Name bereits vergeben'}), 400
try:
saved_query = SavedQuery(
name=name,
description=description,
query_text=query,
user_id=current_user.id
)
db.session.add(saved_query)
db.session.commit()
return jsonify({
'success': True,
'message': 'Query erfolgreich gespeichert',
'query': saved_query.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({'error': f'Fehler beim Speichern: {str(e)}'}), 500
@main_bp.route('/delete_query/<int:query_id>', methods=['DELETE'])
@login_required
def delete_query(query_id):
try:
query = SavedQuery.query.filter_by(id=query_id, user_id=current_user.id).first()
if not query:
return jsonify({'error': 'Query nicht gefunden'}), 404
db.session.delete(query)
db.session.commit()
return jsonify({
'success': True,
'message': 'Query erfolgreich gelöscht'
})
except Exception as e:
db.session.rollback()
return jsonify({'error': f'Fehler beim Löschen: {str(e)}'}), 500
@main_bp.route('/test_connection/<connection_name>')
@login_required
def test_connection(connection_name):
try:
db_manager = DatabaseManager()
result = db_manager.test_connection(connection_name)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Fehler beim Testen der Verbindung: {str(e)}'
}), 500
@main_bp.route('/get_connections')
@login_required
def get_connections():
try:
db_manager = DatabaseManager()
connections = db_manager.get_available_connections()
return jsonify({
'success': True,
'connections': connections
})
except Exception as e:
return jsonify({
'error': f'Fehler beim Laden der Verbindungen: {str(e)}'
}), 500

View File

@@ -0,0 +1,76 @@
from app.services.database_service import DatabaseService
import os
class DatabaseManager:
"""Manager für verschiedene Datenbankverbindungen"""
def __init__(self):
self.connections = {}
self._load_default_connections()
def _load_default_connections(self):
"""Lade Standard-Datenbankverbindungen"""
# Oracle-Datenbank (wenn konfiguriert)
oracle_host = os.environ.get('ORACLE_HOST')
if oracle_host:
self.connections['oracle'] = {
'name': 'Oracle Database',
'type': 'oracle',
'description': 'Oracle-Produktionsdatenbank',
'host': oracle_host,
'port': int(os.environ.get('ORACLE_PORT', 1521)),
'service_name': os.environ.get('ORACLE_SERVICE_NAME', 'ORCL'),
'username': os.environ.get('ORACLE_USERNAME'),
'password': os.environ.get('ORACLE_PASSWORD')
}
# PostgreSQL (wenn konfiguriert)
postgres_host = os.environ.get('POSTGRES_HOST')
if postgres_host:
self.connections['postgres'] = {
'name': 'PostgreSQL Database',
'type': 'postgresql',
'description': 'PostgreSQL-Datenbank',
'host': postgres_host,
'port': int(os.environ.get('POSTGRES_PORT', 5432)),
'database': os.environ.get('POSTGRES_DATABASE', 'postgres'),
'username': os.environ.get('POSTGRES_USERNAME'),
'password': os.environ.get('POSTGRES_PASSWORD')
}
def get_database_service(self, connection_name='demo'):
"""Erstelle DatabaseService für spezifische Verbindung"""
if connection_name not in self.connections:
raise ValueError(f"Datenbankverbindung '{connection_name}' nicht gefunden")
config = self.connections[connection_name]
# Nur Oracle-Verbindungen unterstützt
return DatabaseService(config)
def get_available_connections(self):
"""Hole alle verfügbaren Datenbankverbindungen"""
return [
{
'key': key,
'name': config['name'],
'type': config['type'],
'description': config['description']
}
for key, config in self.connections.items()
]
def test_connection(self, connection_name):
"""Teste eine Datenbankverbindung"""
try:
db_service = self.get_database_service(connection_name)
# Oracle Test-Query
test_query = "SELECT 1 FROM DUAL"
result = db_service.execute_query(test_query)
return {'success': True, 'message': 'Verbindung erfolgreich'}
except Exception as e:
return {'success': False, 'message': str(e)}

View File

@@ -0,0 +1,268 @@
import os
from flask import current_app
import logging
# Oracle-Unterstützung
try:
import oracledb
ORACLE_AVAILABLE = True
except ImportError:
try:
import cx_Oracle
ORACLE_AVAILABLE = True
except ImportError:
ORACLE_AVAILABLE = False
logging.warning("Oracle-Treiber nicht verfügbar. Installiere cx-Oracle oder oracledb für Oracle-Unterstützung.")
# Weitere Datenbank-Treiber
try:
import psycopg2
POSTGRESQL_AVAILABLE = True
except ImportError:
POSTGRESQL_AVAILABLE = False
try:
import pymysql
MYSQL_AVAILABLE = True
except ImportError:
MYSQL_AVAILABLE = False
class DatabaseService:
def __init__(self, connection_config=None):
"""
Initialisiere Database Service - Nur Oracle-Unterstützung
connection_config: Dictionary mit Oracle-Verbindungsparametern
"""
if connection_config is None:
raise ValueError("Oracle-Verbindungskonfiguration erforderlich")
self.connection_config = connection_config
self.db_type = connection_config.get('type', 'oracle')
if self.db_type == 'oracle':
self._validate_oracle_config()
else:
raise ValueError(f"Datenbanktyp '{self.db_type}' wird nicht unterstützt. Nur Oracle ist verfügbar.")
def _validate_oracle_config(self):
"""Validiere Oracle-Konfiguration"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar. Installiere cx-Oracle oder oracledb.")
required_fields = ['host', 'username', 'password']
for field in required_fields:
if not self.connection_config.get(field):
raise Exception(f"Oracle-Konfiguration unvollständig: {field} fehlt")
def execute_query(self, query):
"""Führe Oracle SQL-Query aus und gebe Ergebnisse zurück"""
return self._execute_oracle_query(query)
def _execute_oracle_query(self, query):
"""Führe Oracle Query aus"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar")
# Erstelle Oracle-Verbindung
config = self.connection_config
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
try:
# Verwende den neueren oracledb-Treiber wenn verfügbar, sonst cx_Oracle
try:
import oracledb
conn = oracledb.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
except ImportError:
import cx_Oracle
conn = cx_Oracle.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
cursor = conn.cursor()
try:
# Entferne Semikolon am Ende (Oracle mag das nicht bei einzelnen Statements)
cleaned_query = query.strip()
if cleaned_query.endswith(';'):
cleaned_query = cleaned_query[:-1]
cursor.execute(cleaned_query)
# Wenn es eine SELECT-Query ist, hole Ergebnisse
if query.strip().lower().startswith('select'):
columns = [desc[0] for desc in cursor.description]
data = cursor.fetchall()
# Konvertiere Oracle-spezifische Datentypen
processed_data = []
for row in data:
processed_row = []
for value in row:
# Konvertiere LOB, CLOB, BLOB zu String
if hasattr(value, 'read'):
processed_row.append(value.read())
# Konvertiere Oracle NUMBER zu Python-Typen
elif str(type(value)).find('NUMBER') != -1:
processed_row.append(float(value) if '.' in str(value) else int(value))
else:
processed_row.append(value)
processed_data.append(processed_row)
return {
'columns': columns,
'data': processed_data,
'row_count': len(processed_data)
}
else:
# Für INSERT, UPDATE, DELETE etc.
conn.commit()
return {
'message': f'Query erfolgreich ausgeführt. {cursor.rowcount} Zeilen betroffen.',
'affected_rows': cursor.rowcount
}
except Exception as e:
conn.rollback()
raise e
finally:
cursor.close()
except Exception as e:
raise Exception(f"Oracle-Datenbankfehler: {str(e)}")
finally:
if 'conn' in locals():
conn.close()
def get_tables(self):
"""Hole alle Oracle Tabellennamen"""
return self._get_oracle_tables()
def _get_oracle_tables(self):
"""Hole Oracle Tabellennamen"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar")
config = self.connection_config
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
try:
try:
import oracledb
conn = oracledb.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
except ImportError:
import cx_Oracle
conn = cx_Oracle.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
cursor = conn.cursor()
# Hole alle Tabellen für den aktuellen Benutzer
cursor.execute("""
SELECT table_name
FROM user_tables
ORDER BY table_name
""")
tables = [row[0] for row in cursor.fetchall()]
cursor.close()
conn.close()
return tables
except Exception as e:
raise Exception(f"Fehler beim Laden der Oracle-Tabellen: {str(e)}")
def get_table_schema(self, table_name):
"""Hole Oracle Tabellenschema"""
return self._get_oracle_table_schema(table_name)
def _get_oracle_table_schema(self, table_name):
"""Hole Oracle Tabellenschema"""
if not ORACLE_AVAILABLE:
raise Exception("Oracle-Treiber nicht verfügbar")
config = self.connection_config
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
try:
try:
import oracledb
conn = oracledb.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
except ImportError:
import cx_Oracle
conn = cx_Oracle.connect(
user=config['username'],
password=config['password'],
dsn=dsn
)
cursor = conn.cursor()
# Hole Spalten-Informationen
cursor.execute("""
SELECT
c.column_name,
c.data_type,
c.data_length,
c.data_precision,
c.data_scale,
c.nullable,
c.data_default,
CASE WHEN p.column_name IS NOT NULL THEN 'Y' ELSE 'N' END as primary_key
FROM user_tab_columns c
LEFT JOIN (
SELECT acc.column_name, acc.table_name
FROM user_constraints ac, user_cons_columns acc
WHERE ac.constraint_name = acc.constraint_name
AND ac.constraint_type = 'P'
AND ac.table_name = :table_name
) p ON c.column_name = p.column_name AND c.table_name = p.table_name
WHERE c.table_name = :table_name
ORDER BY c.column_id
""", {'table_name': table_name.upper()})
columns = []
for row in cursor.fetchall():
data_type = row[1]
if row[2]: # data_length
if row[3]: # data_precision
data_type += f"({row[3]}"
if row[4]: # data_scale
data_type += f",{row[4]}"
data_type += ")"
elif row[1] in ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR'):
data_type += f"({row[2]})"
columns.append({
'name': row[0],
'type': data_type,
'not_null': row[5] == 'N',
'default_value': row[6],
'primary_key': row[7] == 'Y'
})
cursor.close()
conn.close()
return columns
except Exception as e:
raise Exception(f"Fehler beim Laden des Oracle-Tabellenschemas: {str(e)}")

753
app/static/css/style.css Normal file
View File

@@ -0,0 +1,753 @@
/* Hauptstyles für die Query Builder Anwendung */
/* Basis-Reset und Layout */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
line-height: 1.5;
}
/* Fallback für Bootstrap Klassen falls CDN nicht lädt */
.container-fluid {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col-md-3 {
flex: 0 0 25%;
max-width: 25%;
padding-right: 15px;
padding-left: 15px;
}
.col-md-9 {
flex: 0 0 75%;
max-width: 75%;
padding-right: 15px;
padding-left: 15px;
}
.h-100 {
height: 100vh !important;
}
/* Navigation Bar */
.navbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background-color: #343a40 !important;
color: white;
margin-bottom: 0;
}
.navbar-brand {
padding-top: 0.3125rem;
padding-bottom: 0.3125rem;
margin-right: 1rem;
font-size: 1.25rem;
color: white !important;
text-decoration: none;
font-weight: bold;
}
.navbar-nav {
display: flex;
flex-direction: row;
list-style: none;
margin: 0;
padding: 0;
}
.navbar-text {
color: rgba(255,255,255,.5) !important;
margin-right: 1rem;
}
.nav-link {
color: rgba(255,255,255,.5) !important;
text-decoration: none;
padding: 0.5rem 1rem;
}
.nav-link:hover {
color: rgba(255,255,255,.75) !important;
}
/* Buttons */
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
cursor: pointer;
background-color: transparent;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
text-decoration: none;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
color: #fff;
background-color: #0056b3;
border-color: #004085;
}
.btn-success {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-success:hover {
color: #fff;
background-color: #1e7e34;
border-color: #1c7430;
}
.btn-secondary {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:hover {
color: #fff;
background-color: #545b62;
border-color: #4e555b;
}
.btn-outline-primary {
color: #007bff;
border-color: #007bff;
background-color: transparent;
}
.btn-outline-primary:hover {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-outline-success {
color: #28a745;
border-color: #28a745;
background-color: transparent;
}
.btn-outline-success:hover {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-outline-danger {
color: #dc3545;
border-color: #dc3545;
background-color: transparent;
}
.btn-outline-danger:hover {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-sm {
padding: 0.15rem 0.4rem;
font-size: 0.75rem;
border-radius: 0.15rem;
line-height: 1.2;
}
.btn-xs {
padding: 0.1rem 0.25rem;
font-size: 0.65rem;
border-radius: 0.1rem;
line-height: 1.1;
}
.btn-group {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.btn-group > .btn {
position: relative;
flex: 1 1 auto;
margin-right: -1px;
}
.btn-group > .btn:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group > .btn:not(:first-child):not(:last-child) {
border-radius: 0;
}
.btn-group > .btn:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Cards */
.card {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.card-body {
flex: 1 1 auto;
padding: 1.25rem;
}
.card-header {
padding: 0.75rem 1.25rem;
margin-bottom: 0;
background-color: rgba(0,0,0,.03);
border-bottom: 1px solid rgba(0,0,0,.125);
border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.card-footer {
padding: 0.75rem 1.25rem;
background-color: rgba(0,0,0,.03);
border-top: 1px solid rgba(0,0,0,.125);
}
.card-title {
margin-bottom: 0.75rem;
font-size: 1.25rem;
font-weight: 500;
}
.card-text {
margin-bottom: 1rem;
}
/* Forms */
.form-control {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.form-label {
margin-bottom: 0.5rem;
font-weight: 500;
}
/* Alerts */
.alert {
position: relative;
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-dismissible {
padding-right: 4rem;
}
/* Tables */
.table {
width: 100%;
margin-bottom: 1rem;
color: #212529;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
.table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
background-color: #f8f9fa;
font-weight: 600;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,.05);
}
.table-hover tbody tr:hover {
color: #212529;
background-color: rgba(0,0,0,.075);
}
/* Utilities */
.text-center { text-align: center !important; }
.text-muted { color: #6c757d !important; }
.text-danger { color: #dc3545 !important; }
.text-success { color: #28a745 !important; }
.text-primary { color: #007bff !important; }
.text-info { color: #17a2b8 !important; }
.text-warning { color: #ffc107 !important; }
.bg-light { background-color: #f8f9fa !important; }
.bg-dark { background-color: #343a40 !important; }
.border-end { border-right: 1px solid #dee2e6 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mb-1 { margin-bottom: 0.25rem !important; }
.mb-2 { margin-bottom: 0.5rem !important; }
.mb-3 { margin-bottom: 1rem !important; }
.mb-4 { margin-bottom: 1.5rem !important; }
.mt-2 { margin-top: 0.5rem !important; }
.mt-3 { margin-top: 1rem !important; }
.mt-5 { margin-top: 3rem !important; }
.p-2 { padding: 0.5rem !important; }
.p-3 { padding: 1rem !important; }
.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }
.justify-content-between { justify-content: space-between !important; }
.justify-content-center { justify-content: center !important; }
.align-items-center { align-items: center !important; }
.float-end { float: right !important; }
.ms-auto { margin-left: auto !important; }
.me-3 { margin-right: 1rem !important; }
/* Sidebar Styles */
.col-md-3.border-end {
height: calc(100vh - 56px);
overflow-y: auto;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
}
.col-md-9 {
height: calc(100vh - 56px);
overflow-y: auto;
}
/* Tabellen und Query Cards */
.saved-query-card {
transition: all 0.2s ease;
cursor: pointer;
}
.saved-query-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.table-container {
margin-bottom: 4px;
}
.table-item {
padding: 4px 8px;
margin: 1px 0;
background: white;
border: 1px solid #dee2e6;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
}
.table-item:hover {
background: #f8f9fa;
border-color: #007bff;
}
.table-item.selected {
background: #e3f2fd;
border-color: #2196f3;
font-weight: 600;
}
.table-item i {
color: #6c757d;
margin-right: 6px;
font-size: 0.8rem;
}
/* Spalten-Anzeige unter Tabellen */
.table-columns {
margin-left: 8px;
margin-top: 2px;
padding: 4px 6px;
background: #f8f9fa;
border-left: 2px solid #007bff;
border-radius: 0 3px 3px 0;
}
.column-item {
padding: 2px 6px;
margin: 0.5px 0;
background: white;
border: 1px solid #e9ecef;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 11px;
display: flex;
align-items: center;
gap: 4px;
line-height: 1.2;
}
.column-item:hover {
background: #e7f3ff;
border-color: #007bff;
}
.column-name {
font-weight: 500;
color: #495057;
}
.column-type {
font-size: 9px;
color: #6c757d;
margin-left: auto;
}
/* Toggle-Button für Spalten-Anzeige */
#toggle-columns-btn {
margin-right: 3px;
transition: all 0.3s ease;
padding: 0.1rem 0.3rem;
font-size: 0.7rem;
}
#toggle-columns-btn:hover {
transform: scale(1.05);
}
#toggle-columns-btn.btn-outline-secondary {
opacity: 0.7;
}
#toggle-columns-btn.btn-outline-primary {
opacity: 1;
}
/* Drag & Drop Styles */
.table-item, .column-item {
cursor: grab;
}
.table-item:active, .column-item:active {
cursor: grabbing;
}
.drag-icon {
opacity: 0;
transition: opacity 0.2s ease;
margin-left: auto;
color: #6c757d;
font-size: 10px;
}
.table-item:hover .drag-icon,
.column-item:hover .drag-icon {
opacity: 0.7;
}
/* Drop Zone Styles */
#query-input.drag-over {
border-color: #007bff !important;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.3) !important;
background-color: #f8f9ff !important;
}
.table-item[draggable="true"]:hover,
.column-item[draggable="true"]:hover {
background-color: #e3f2fd;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Query Input Area */
#query-input {
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
min-height: 150px;
}
/* Results Table */
.results-table {
max-height: 400px;
overflow: auto;
}
.results-table table {
font-size: 14px;
}
.results-table th {
position: sticky;
top: 0;
background: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
/* Loading Spinner */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Alert Styles */
.alert-custom {
margin: 0 0 15px 0;
padding: 10px 15px;
border-radius: 4px;
}
.alert-success-custom {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-error-custom {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
/* Responsive Design */
@media (max-width: 768px) {
.col-md-3.border-end {
height: auto;
max-height: 300px;
}
.col-md-9 {
height: auto;
}
body {
overflow: auto;
}
.container-fluid {
height: auto;
overflow: auto;
}
.row.h-100 {
height: auto;
}
}
/* Button Improvements */
.btn-group-sm > .btn, .btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
/* Card Improvements */
.card {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
/* Navigation Improvements */
.navbar-brand {
font-weight: bold;
}
/* Modal Improvements */
.modal-content {
border-radius: 8px;
}
.modal-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
/* Table Schema Popup */
.schema-popup {
position: absolute;
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
z-index: 1000;
max-width: 300px;
font-size: 12px;
}
/* Autocomplete Dropdown */
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #007bff;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1050;
max-height: 200px;
overflow-y: auto;
display: none;
}
.autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s ease;
font-size: 0.9rem;
display: flex;
align-items: center;
}
.autocomplete-item:last-child {
border-bottom: none;
}
.autocomplete-item:hover {
background-color: #f8f9fa;
}
.autocomplete-item.selected {
background-color: #e3f2fd;
color: #1976d2;
}
.autocomplete-item i {
margin-right: 8px;
color: #6c757d;
font-size: 0.8rem;
}
.autocomplete-item small {
margin-left: auto;
font-size: 0.75rem;
color: #6c757d;
}
.autocomplete-item strong {
flex: 1;
}
.autocomplete-header {
padding: 6px 12px;
background-color: #f8f9fa;
font-size: 0.75rem;
font-weight: 600;
color: #495057;
border-bottom: 1px solid #dee2e6;
}
.schema-column {
padding: 2px 0;
border-bottom: 1px solid #f0f0f0;
}
.schema-column:last-child {
border-bottom: none;
}

File diff suppressed because one or more lines are too long

9
app/static/css/vendor/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
app/static/favicon.ico Normal file
View File

@@ -0,0 +1 @@
# Placeholder für favicon - in Produktion sollte hier eine echte .ico Datei sein

1628
app/static/js/app.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Login - Query Builder{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="text-center mb-0">
<i class="fas fa-sign-in-alt"></i> Anmelden
</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> Anmelden
</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('auth.register') }}" class="text-decoration-none">
Noch kein Konto? Hier registrieren
</a>
</div>
</div>
<div class="mt-3 p-3 bg-light rounded">
<h6>Demo-Zugang:</h6>
<p class="small mb-1"><strong>Benutzername:</strong> admin</p>
<p class="small mb-0"><strong>Passwort:</strong> admin123</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Registrierung - Query Builder{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="text-center mb-0">
<i class="fas fa-user-plus"></i> Registrierung
</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">
<i class="fas fa-user-plus"></i> Registrieren
</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('auth.login') }}" class="text-decoration-none">
Bereits ein Konto? Hier anmelden
</a>
</div>
</div>
</div>
</div>
{% endblock %}

61
app/templates/base.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Query Builder{% endblock %}</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Bootstrap CSS (lokal gehostet) -->
<link href="{{ url_for('static', filename='css/vendor/bootstrap.min.css') }}" rel="stylesheet">
<!-- Font Awesome Icons (lokal gehostet) -->
<link href="{{ url_for('static', filename='css/vendor/all-local.min.css') }}" rel="stylesheet">
<!-- Custom Styles (Fallback für Bootstrap + eigene Styles) -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.dashboard') }}">
<i class="fas fa-database"></i> Query Builder
</a>
{% if current_user.is_authenticated %}
<div class="navbar-nav ms-auto">
<span class="navbar-text me-3">
<i class="fas fa-user"></i> {{ current_user.username }}
</span>
<a class="nav-link" href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt"></i> Abmelden
</a>
</div>
{% endif %}
</div>
</nav>
<main class="container-fluid">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="row mt-3">
<div class="col-12">
{% for message in messages %}
<div class="alert alert-info alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<!-- Bootstrap JS (lokal gehostet) -->
<script src="{{ url_for('static', filename='js/vendor/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,255 @@
{% extends "base.html" %}
{% block title %}Dashboard - Query Builder{% endblock %}
{% block content %}
<div class="row h-100">
<!-- Linke Sidebar - Tabellen -->
<div class="col-md-2 border-end bg-light">
<div class="p-3">
<!-- Datenbankauswahl Section -->
<div class="mb-4">
<h6><i class="fas fa-database"></i> Datenbankverbindung</h6>
<select id="database-selector" class="form-select form-select-sm" onchange="onDatabaseChange()">
<option value="demo">Lade Verbindungen...</option>
</select>
<button class="btn btn-sm btn-outline-info mt-2 w-100" onclick="testConnection()">
<i class="fas fa-plug"></i> Verbindung testen
</button>
</div>
<!-- Tabellen Section -->
<div class="mb-4">
<h5>
<i class="fas fa-table"></i> Tabellen
<div class="float-end">
<button class="btn btn-sm btn-outline-secondary" id="toggle-columns-btn" onclick="toggleColumnsView()" title="Spalten ein/ausblenden">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="loadTables()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</h5>
<div id="tables-list">
<div class="text-muted">Lade Tabellen...</div>
</div>
</div>
</div>
</div>
<!-- Hauptbereich - Query Editor und Ergebnisse -->
<!-- Mittlere Spalte - Query Editor und Ergebnisse -->
<div class="col-md-8">
<div class="p-3">
<!-- Query Editor Section -->
<div class="mb-4">
<h5><i class="fas fa-code"></i> SQL Query Editor</h5>
<div class="card">
<div class="card-body">
<div class="position-relative">
<textarea id="query-input" class="form-control mb-3" rows="8"
placeholder="Geben Sie hier Ihre Oracle SQL-Query ein...&#10;&#10;Beispiel für Oracle:&#10;SELECT * FROM USERS WHERE ROWNUM <= 10"></textarea>
<div id="autocomplete-dropdown" class="autocomplete-dropdown"></div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="executeQuery()">
<i class="fas fa-play"></i> Query Ausführen
</button>
<button type="button" class="btn btn-success" id="save-query-btn">
<i class="fas fa-save"></i> Query Speichern
</button>
<button class="btn btn-secondary" onclick="clearQuery()">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
</div>
<!-- Ergebnisse Section -->
<div class="mb-3">
<h5><i class="fas fa-table"></i> Query-Ergebnisse</h5>
<div id="results-container" class="card">
<div class="card-body">
<div class="text-muted text-center py-4">
Führen Sie eine Query aus, um Ergebnisse anzuzeigen
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Rechte Sidebar - Gespeicherte Queries -->
<div class="col-md-2 border-start bg-light">
<div class="p-2 h-100">
<div class="mb-3">
<h6><i class="fas fa-bookmark"></i> Gespeicherte Queries</h6>
</div>
<!-- Query-Suche -->
<div class="mb-3">
<input type="text" id="query-search" class="form-control form-control-sm"
placeholder="Query suchen..." onkeyup="filterSavedQueries()">
</div>
<!-- Gespeicherte Queries Liste -->
<div id="saved-queries-list" class="overflow-auto" style="max-height: calc(100vh - 250px);">
{% for query in saved_queries %}
<div class="card mb-2 saved-query-card" data-query-id="{{ query.id }}">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1" style="cursor: pointer;" onclick="loadSavedQuery({{ query.id }})">
<h6 class="card-title mb-1 text-primary">{{ query.name }}</h6>
<small class="text-muted d-block mb-1">{{ query.created_at.strftime('%d.%m.%Y %H:%M') }}</small>
<small class="text-muted query-preview">{{ query.query[:100] }}{% if query.query|length > 100 %}...{% endif %}</small>
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="loadSavedQuery({{ query.id }})">
<i class="fas fa-play"></i> Laden
</a></li>
<li><a class="dropdown-item" href="#" onclick="editQueryName({{ query.id }})">
<i class="fas fa-edit"></i> Umbenennen
</a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateQuery({{ query.id }})">
<i class="fas fa-copy"></i> Duplizieren
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="deleteSavedQuery({{ query.id }})">
<i class="fas fa-trash"></i> Löschen
</a></li>
</ul>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- Platzhalter wenn keine Queries vorhanden -->
<div id="no-queries-placeholder" class="text-center text-muted py-4" {% if saved_queries %}style="display: none;"{% endif %}>
<i class="fas fa-bookmark fa-2x mb-2"></i>
<p>Keine gespeicherten Queries vorhanden.</p>
<small>Speichern Sie Ihre erste Query mit dem <i class="fas fa-save"></i> Button!</small>
</div>
</div>
</div>
</div>
</div>
<!-- Save Query Modal -->
<div class="modal fade" id="saveQueryModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Query speichern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="query-name" class="form-label">Query-Name:</label>
<input type="text" class="form-control" id="query-name"
placeholder="z.B. Benutzer-Liste" required autofocus>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-success" onclick="saveQuery()">
<i class="fas fa-save"></i> Speichern
</button>
</div>
</div>
</div>
</div>
<!-- Edit Query Name Modal -->
<div class="modal fade" id="editQueryModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Query umbenennen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="edit-query-name" class="form-label">Neuer Name:</label>
<input type="text" class="form-control" id="edit-query-name" required autofocus>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="updateQueryName()">
<i class="fas fa-save"></i> Umbenennen
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Verbindungen werden automatisch in app.js geladen
// currentConnection ist jetzt in app.js definiert
// Die loadConnections und onDatabaseChange Funktionen sind jetzt in app.js
// Teste Datenbankverbindung
function testConnection() {
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Teste...';
button.disabled = true;
fetch(`/test_connection/${currentConnection}`)
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', `${data.message}`);
} else {
showAlert('danger', `${data.message}`);
}
})
.catch(error => {
showAlert('danger', `Fehler beim Testen der Verbindung: ${error}`);
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
// Hilfsfunktion für Alerts
function showAlert(type, message) {
// Entferne vorhandene Alerts
const existingAlert = document.querySelector('.alert');
if (existingAlert) {
existingAlert.remove();
}
// Erstelle neues Alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Füge Alert am Anfang des Contents ein
const content = document.querySelector('.container-fluid');
content.insertBefore(alertDiv, content.firstChild);
// Auto-hide nach 5 Sekunden
setTimeout(() => {
if (alertDiv && alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
</script>
{% endblock %}