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.
This commit is contained in:
452
backend/app/blueprints/api.py
Normal file
452
backend/app/blueprints/api.py
Normal file
@@ -0,0 +1,452 @@
|
||||
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
|
Reference in New Issue
Block a user