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:
2025-05-25 20:33:38 +02:00
parent e21104611f
commit 2d33753b94
1288 changed files with 247388 additions and 3249 deletions

View File

@@ -4,7 +4,11 @@ import logging
from typing import Dict, Callable, Any, List, Optional, Union
from datetime import datetime, timedelta
from PyP100 import PyP110
from sqlalchemy.orm import joinedload
from utils.logging_config import get_logger
from models import Job, Printer, get_db_session
# Lazy logger initialization
_logger = None
@@ -27,6 +31,7 @@ class BackgroundTaskScheduler:
self._thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
self._running = False
self._start_time: Optional[datetime] = None
def register_task(self,
task_id: str,
@@ -165,6 +170,45 @@ class BackgroundTaskScheduler:
for tid, task in self._tasks.items()
]
def get_tasks(self) -> Dict[str, Dict[str, Any]]:
"""
Gibt alle Tasks mit ihren Konfigurationen zurück.
Returns:
Dict: Dictionary mit Task-IDs als Schlüssel und Task-Konfigurationen als Werte
"""
return {
task_id: {
"interval": task["interval"],
"enabled": task["enabled"],
"last_run": task["last_run"].isoformat() if task["last_run"] else None,
"next_run": task["next_run"].isoformat() if task["next_run"] else None
}
for task_id, task in self._tasks.items()
}
def get_uptime(self) -> Optional[str]:
"""
Gibt die Laufzeit des Schedulers seit dem Start zurück.
Returns:
str: Formatierte Laufzeit oder None, wenn der Scheduler nicht läuft
"""
if not self._running or not self._start_time:
return None
uptime = datetime.now() - self._start_time
days = uptime.days
hours, remainder = divmod(uptime.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
return f"{days} Tage, {hours} Stunden, {minutes} Minuten"
elif hours > 0:
return f"{hours} Stunden, {minutes} Minuten"
else:
return f"{minutes} Minuten, {seconds} Sekunden"
def start(self) -> bool:
"""
Startet den Scheduler.
@@ -182,6 +226,7 @@ class BackgroundTaskScheduler:
self._thread.daemon = True
self._thread.start()
self._running = True
self._start_time = datetime.now()
logger.info("Scheduler gestartet")
return True
@@ -203,6 +248,7 @@ class BackgroundTaskScheduler:
self._thread.join(timeout=5.0)
self._running = False
self._start_time = None
logger.info("Scheduler gestoppt")
return True
@@ -216,29 +262,150 @@ class BackgroundTaskScheduler:
return self._running
def _run(self) -> None:
"""Interne Methode zum Ausführen des Scheduler-Loops."""
"""Hauptloop des Schedulers."""
logger = get_scheduler_logger()
logger.info("Scheduler-Thread gestartet")
while not self._stop_event.is_set():
now = datetime.now()
for task_id, task in self._tasks.items():
if not task["enabled"] or not task["next_run"]:
continue
if now >= task["next_run"]:
logger = get_scheduler_logger()
logger.info(f"Ausführung von Task {task_id}")
if now >= task["next_run"]:
try:
logger.debug(f"Führe Task {task_id} aus")
task["func"](*task["args"], **task["kwargs"])
logger.info(f"Task {task_id} erfolgreich ausgeführt")
task["last_run"] = now
task["next_run"] = now + timedelta(seconds=task["interval"])
logger.debug(f"Task {task_id} erfolgreich ausgeführt, nächste Ausführung: {task['next_run']}")
except Exception as e:
logger.error(f"Fehler bei Ausführung von Task {task_id}: {str(e)}")
task["last_run"] = now
task["next_run"] = now + timedelta(seconds=task["interval"])
# 1 Sekunde warten und erneut prüfen
self._stop_event.wait(1)
# Trotzdem nächste Ausführung planen
task["next_run"] = now + timedelta(seconds=task["interval"])
# Schlafenszeit berechnen (1 Sekunde oder weniger)
time.sleep(1)
logger.info("Scheduler-Thread beendet")
# Singleton-Instanz
scheduler = BackgroundTaskScheduler()
def toggle_plug(printer_id: int, state: bool) -> bool:
"""
Schaltet eine Tapo-Steckdose ein oder aus.
Args:
printer_id: ID des Druckers
state: True für ein, False für aus
Returns:
bool: True wenn erfolgreich, False wenn fehlgeschlagen
"""
logger = get_logger("printers")
db_session = get_db_session()
try:
printer = db_session.query(Printer).get(printer_id)
if not printer:
logger.error(f"Drucker mit ID {printer_id} nicht gefunden")
db_session.close()
return False
# TP-Link Tapo P110 Verbindung herstellen
p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password)
p110.handshake() # Authentifizierung
p110.login() # Login
# Steckdose ein-/ausschalten
if state:
p110.turnOn()
logger.info(f"Steckdose für Drucker {printer.name} (ID: {printer_id}) eingeschaltet")
else:
p110.turnOff()
logger.info(f"Steckdose für Drucker {printer.name} (ID: {printer_id}) ausgeschaltet")
db_session.close()
return True
except Exception as e:
logger.error(f"Fehler beim Schalten der Steckdose für Drucker {printer_id}: {str(e)}")
db_session.close()
return False
def check_jobs():
"""
Überprüft alle geplanten und laufenden Jobs und schaltet Steckdosen entsprechend.
Diese Funktion wird vom Scheduler regelmäßig aufgerufen und:
1. Prüft, ob geplante Jobs gestartet werden müssen
2. Prüft, ob laufende Jobs beendet werden müssen
3. Aktualisiert den Status der Jobs
"""
logger = get_logger("jobs")
db_session = get_db_session()
try:
now = datetime.now()
# Geplante Jobs abrufen (mit 5 Minuten Puffer für vergangene Jobs)
scheduled_jobs = db_session.query(Job).options(
joinedload(Job.printer)
).filter(
Job.status == "scheduled",
Job.start_at <= now
).all()
# Laufende Jobs abrufen (mit 5 Minuten Sicherheitspuffer)
running_jobs = db_session.query(Job).options(
joinedload(Job.printer)
).filter(
Job.status == "running",
Job.end_at <= now - timedelta(minutes=5) # 5 Minuten Sicherheitspuffer
).all()
# Geplante Jobs starten
for job in scheduled_jobs:
logger.info(f"Starte geplanten Job {job.id}: {job.name} für Drucker {job.printer.name}")
# Steckdose einschalten
if toggle_plug(job.printer_id, True):
# Job als laufend markieren
job.status = "running"
job.end_at = job.start_at + timedelta(minutes=job.duration_minutes)
db_session.commit()
logger.info(f"Job {job.id} gestartet: läuft bis {job.end_at}")
else:
logger.error(f"Fehler beim Starten von Job {job.id}: Steckdose konnte nicht eingeschaltet werden")
# Beendete Jobs stoppen
for job in running_jobs:
logger.info(f"Beende laufenden Job {job.id}: {job.name} für Drucker {job.printer.name}")
# Steckdose ausschalten
if toggle_plug(job.printer_id, False):
# Job als beendet markieren
job.status = "finished"
job.actual_end_time = now
db_session.commit()
logger.info(f"Job {job.id} beendet: tatsächliche Endzeit {job.actual_end_time}")
else:
logger.error(f"Fehler beim Beenden von Job {job.id}: Steckdose konnte nicht ausgeschaltet werden")
db_session.close()
except Exception as e:
logger.error(f"Fehler im Job-Scheduler: {str(e)}")
db_session.close()
# Globaler Scheduler
scheduler = BackgroundTaskScheduler()
# Job-Überprüfungs-Task registrieren (alle 60 Sekunden)
scheduler.register_task(
task_id="check_jobs",
func=check_jobs,
interval=60,
enabled=True
)