Till Tomczak 2d33753b94 feat: Major updates to backend structure and security enhancements
- Removed `COMMON_ERRORS.md` file to streamline documentation.
- Added `Flask-Limiter` for rate limiting and `redis` for session management in `requirements.txt`.
- Expanded `ROADMAP.md` to include completed security features and planned enhancements for version 2.2.
- Enhanced `setup_myp.sh` for ultra-secure kiosk installation, including system hardening and security configurations.
- Updated `app.py` to integrate CSRF protection and improved logging setup.
- Refactored user model to include username and active status for better user management.
- Improved job scheduler with uptime tracking and task management features.
- Updated various templates for a more cohesive user interface and experience.
2025-05-25 20:33:38 +02:00

452 lines
15 KiB
Python

from flask import Blueprint, jsonify, request, session, current_app
from datetime import datetime, timedelta
import logging
import uuid
import json
import os
from models import User, Job, Printer, SystemLog
from database.db_manager import DatabaseManager
from flask_login import login_required, current_user
api_bp = Blueprint('api', __name__, url_prefix='/api')
logger = logging.getLogger(__name__)
@api_bp.route('/jobs')
def get_jobs():
"""Get jobs with optional filtering"""
try:
db = DatabaseManager()
# Get query parameters
status = request.args.get('status')
limit = request.args.get('limit', type=int)
# Use a session for proper relationship handling
session = db.get_session()
try:
from sqlalchemy.orm import joinedload
# Get jobs based on filters with eager loading of relationships
if status == 'active':
jobs_query = session.query(Job).options(
joinedload(Job.user),
joinedload(Job.printer)
).filter(Job.status == 'running')
jobs = jobs_query.all() or []
# Also include pending jobs
pending_jobs_query = session.query(Job).options(
joinedload(Job.user),
joinedload(Job.printer)
).filter(Job.status == 'pending')
pending_jobs = pending_jobs_query.all() or []
jobs.extend(pending_jobs)
else:
jobs_query = session.query(Job).options(
joinedload(Job.user),
joinedload(Job.printer)
)
jobs = jobs_query.all() or []
# Apply limit
if limit:
jobs = jobs[:limit]
# Format jobs for API response - convert all data while session is open
formatted_jobs = []
for job in jobs:
if hasattr(job, '__dict__'):
# Access all relationship data here while the session is still open
printer_name = job.printer.name if job.printer else 'Kein Drucker'
user_name = job.user.name if job.user else 'Kein Benutzer'
job_data = {
'id': getattr(job, 'id', None),
'name': getattr(job, 'name', 'Unbenannter Auftrag'),
'status': getattr(job, 'status', 'unknown'),
'progress': getattr(job, 'progress', 0),
'printer_name': printer_name,
'user_name': user_name,
'created_at': getattr(job, 'created_at', datetime.now()).isoformat() if hasattr(job, 'created_at') else datetime.now().isoformat(),
'estimated_time': getattr(job, 'estimated_time', 0),
'print_time': getattr(job, 'print_time', 0)
}
formatted_jobs.append(job_data)
finally:
# Always close the session
session.close()
return jsonify(formatted_jobs)
except Exception as e:
logger.error(f"Error getting jobs: {str(e)}")
return jsonify([])
@api_bp.route('/printers', methods=['GET'])
def get_printers():
"""Get all printers"""
try:
db = DatabaseManager()
session = db.get_session()
try:
printers = session.query(Printer).all() or []
# Format printers for API response
formatted_printers = []
for printer in printers:
printer_data = {
'id': printer.id,
'name': printer.name,
'model': printer.model,
'location': printer.location,
'mac_address': printer.mac_address,
'plug_ip': printer.plug_ip,
'status': printer.status,
'created_at': printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
}
formatted_printers.append(printer_data)
return jsonify({'printers': formatted_printers})
finally:
session.close()
except Exception as e:
logger.error(f"Error getting printers: {str(e)}")
return jsonify({'printers': []})
@api_bp.route('/printers', methods=['POST'])
@login_required
def add_printer():
"""Add a new printer"""
# Überprüfe, ob der Benutzer Admin-Rechte hat
if not current_user.is_admin:
return jsonify({'error': 'Nur Administratoren können Drucker hinzufügen'}), 403
try:
# Hole die Daten aus dem Request
data = request.json
if not data:
return jsonify({'error': 'Keine Daten erhalten'}), 400
# Überprüfe, ob alle notwendigen Felder vorhanden sind
required_fields = ['name', 'model', 'location', 'mac_address', 'plug_ip']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Feld {field} fehlt'}), 400
# Erstelle ein neues Printer-Objekt
new_printer = Printer(
name=data['name'],
model=data['model'],
location=data['location'],
mac_address=data['mac_address'],
plug_ip=data['plug_ip'],
plug_username='admin', # Default-Werte für Plug-Credentials
plug_password='admin', # In einer Produktionsumgebung sollten diese sicher gespeichert werden
status='available'
)
# Speichere den Drucker in der Datenbank
db = DatabaseManager()
session = db.get_session()
try:
session.add(new_printer)
session.commit()
# Gib die Drucker-Daten zurück
printer_data = {
'id': new_printer.id,
'name': new_printer.name,
'model': new_printer.model,
'location': new_printer.location,
'mac_address': new_printer.mac_address,
'plug_ip': new_printer.plug_ip,
'status': new_printer.status,
'created_at': new_printer.created_at.isoformat() if new_printer.created_at else datetime.now().isoformat()
}
return jsonify({'printer': printer_data, 'message': 'Drucker erfolgreich hinzugefügt'}), 201
except Exception as e:
session.rollback()
logger.error(f"Error adding printer: {str(e)}")
return jsonify({'error': f'Fehler beim Hinzufügen des Druckers: {str(e)}'}), 500
finally:
session.close()
except Exception as e:
logger.error(f"Error adding printer: {str(e)}")
return jsonify({'error': f'Fehler beim Hinzufügen des Druckers: {str(e)}'}), 500
@api_bp.route('/printers/<int:printer_id>', methods=['DELETE'])
@login_required
def delete_printer(printer_id):
"""Delete a printer by ID"""
# Überprüfe, ob der Benutzer Admin-Rechte hat
if not current_user.is_admin:
return jsonify({'error': 'Nur Administratoren können Drucker löschen'}), 403
try:
db = DatabaseManager()
session = db.get_session()
try:
# Finde den Drucker
printer = session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
return jsonify({'error': f'Drucker mit ID {printer_id} nicht gefunden'}), 404
# Lösche den Drucker
session.delete(printer)
session.commit()
return jsonify({'message': 'Drucker erfolgreich gelöscht'}), 200
except Exception as e:
session.rollback()
logger.error(f"Error deleting printer: {str(e)}")
return jsonify({'error': f'Fehler beim Löschen des Druckers: {str(e)}'}), 500
finally:
session.close()
except Exception as e:
logger.error(f"Error deleting printer: {str(e)}")
return jsonify({'error': f'Fehler beim Löschen des Druckers: {str(e)}'}), 500
# API-Route für Admin-Statistiken
@api_bp.route('/admin/stats', methods=['GET'])
@login_required
def get_admin_stats():
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
# Statistikdaten sammeln
total_users = User.query.count()
total_printers = Printer.query.count()
active_jobs = Job.query.filter_by(status='running').count()
# Erfolgsrate berechnen
total_completed_jobs = Job.query.filter_by(status='completed').count()
total_jobs = Job.query.filter(Job.status.in_(['completed', 'failed'])).count()
success_rate = "0%"
if total_jobs > 0:
success_rate = f"{int((total_completed_jobs / total_jobs) * 100)}%"
# Scheduler-Status (Beispiel)
scheduler_status = True # In einer echten Anwendung würde hier der tatsächliche Status geprüft
# Systeminformationen
system_stats = {
"cpu_usage": "25%",
"memory_usage": "40%",
"disk_usage": "30%",
"uptime": "3d 12h 45m"
}
# Drucker-Status
printer_status = {
"available": Printer.query.filter_by(status='available').count(),
"busy": Printer.query.filter_by(status='busy').count(),
"offline": Printer.query.filter_by(status='offline').count(),
"maintenance": Printer.query.filter_by(status='maintenance').count()
}
return jsonify({
"total_users": total_users,
"total_printers": total_printers,
"active_jobs": active_jobs,
"success_rate": success_rate,
"scheduler_status": scheduler_status,
"system_stats": system_stats,
"printer_status": printer_status,
"last_updated": datetime.now().isoformat()
})
# API-Route für Protokolle
@api_bp.route('/logs', methods=['GET'])
@login_required
def get_logs():
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
# Logs aus der Datenbank abrufen
logs = SystemLog.query.order_by(SystemLog.timestamp.desc()).limit(100).all()
log_data = []
for log in logs:
log_data.append({
"id": log.id,
"timestamp": log.timestamp.isoformat(),
"level": log.level,
"source": log.source,
"message": log.message
})
return jsonify({"logs": log_data})
# Scheduler-Status abrufen
@api_bp.route('/scheduler/status', methods=['GET'])
@login_required
def get_scheduler_status():
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
# In einer echten Anwendung würde hier der tatsächliche Status geprüft werden
scheduler_active = True
# Aktuelle Aufgaben (Beispiel)
tasks = [
{
"id": 1,
"name": "Druckaufträge verarbeiten",
"status": "running",
"last_run": datetime.now().isoformat(),
"next_run": datetime.now().isoformat()
},
{
"id": 2,
"name": "Status-Updates sammeln",
"status": "idle",
"last_run": datetime.now().isoformat(),
"next_run": datetime.now().isoformat()
}
]
return jsonify({
"active": scheduler_active,
"tasks": tasks,
"last_updated": datetime.now().isoformat()
})
# Scheduler starten
@api_bp.route('/scheduler/start', methods=['POST'])
@login_required
def start_scheduler():
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
# In einer echten Anwendung würde hier der Scheduler gestartet werden
return jsonify({
"success": True,
"message": "Scheduler started successfully",
"active": True
})
# Scheduler stoppen
@api_bp.route('/scheduler/stop', methods=['POST'])
@login_required
def stop_scheduler():
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
# In einer echten Anwendung würde hier der Scheduler gestoppt werden
return jsonify({
"success": True,
"message": "Scheduler stopped successfully",
"active": False
})
# Benutzer abrufen
@api_bp.route('/users', methods=['GET'])
@login_required
def get_users():
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
users = User.query.all()
result = []
for user in users:
result.append({
'id': user.id,
'name': user.name,
'email': user.email,
'is_admin': user.is_admin,
'is_active': user.is_active,
'created_at': user.created_at.isoformat()
})
return jsonify({"users": result})
# Neuen Benutzer hinzufügen
@api_bp.route('/users', methods=['POST'])
@login_required
def add_user():
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
data = request.json
if not data:
return jsonify({"error": "No data provided"}), 400
required_fields = ['name', 'email', 'password']
for field in required_fields:
if field not in data:
return jsonify({"error": f"Missing required field: {field}"}), 400
# Prüfen, ob E-Mail bereits existiert
existing_user = User.query.filter_by(email=data['email']).first()
if existing_user:
return jsonify({"error": "Email already exists"}), 400
# Neuen Benutzer erstellen
user = User(
name=data['name'],
email=data['email'],
is_admin=data.get('role') == 'admin',
is_active=data.get('status') == 'active'
)
user.set_password(data['password'])
try:
db = DatabaseManager()
session = db.get_session()
session.add(user)
session.commit()
return jsonify({
"success": True,
"message": "User added successfully",
"user": {
"id": user.id,
"name": user.name,
"email": user.email,
"is_admin": user.is_admin,
"is_active": user.is_active,
"created_at": user.created_at.isoformat()
}
}), 201
except Exception as e:
db.get_session().rollback()
return jsonify({"error": str(e)}), 500
# Benutzer löschen
@api_bp.route('/users/<int:user_id>', methods=['DELETE'])
@login_required
def delete_user(user_id):
if not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
# Benutzer kann sich nicht selbst löschen
if current_user.id == user_id:
return jsonify({"error": "Cannot delete yourself"}), 400
user = User.query.get(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
try:
db = DatabaseManager()
session = db.get_session()
session.delete(user)
session.commit()
return jsonify({"success": True, "message": "User deleted successfully"})
except Exception as e:
session.rollback()
return jsonify({"error": str(e)}), 500