ein-dateien backend erstellt
This commit is contained in:
parent
68a1910bdc
commit
55936c81f0
37
backend/.env.example
Normal file
37
backend/.env.example
Normal file
@ -0,0 +1,37 @@
|
||||
# Umgebungsvariablen für die MYP Backend-Anwendung
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Sicherheit
|
||||
SECRET_KEY=change_me_in_production
|
||||
|
||||
# Datenbank
|
||||
# SQLite (Standard für Entwicklung)
|
||||
DATABASE_URL=sqlite:///myp.db
|
||||
|
||||
# Für PostgreSQL (empfohlen für Produktion)
|
||||
# DATABASE_URL=postgresql://username:password@localhost/myp
|
||||
|
||||
# Server-Konfiguration
|
||||
# Debug-Modus (True für Entwicklung, False für Produktion)
|
||||
FLASK_DEBUG=True
|
||||
|
||||
# Port (Standard: 5000)
|
||||
PORT=5000
|
||||
|
||||
# Host (0.0.0.0 um von außen erreichbar zu sein)
|
||||
HOST=0.0.0.0
|
||||
|
||||
# GitHub OAuth Konfiguration
|
||||
OAUTH_CLIENT_ID=your_github_client_id
|
||||
OAUTH_CLIENT_SECRET=your_github_client_secret
|
||||
# Wenn du GitHub Enterprise verwendest, passe folgende URLs an:
|
||||
GITHUB_API_BASE_URL=https://api.github.com/
|
||||
GITHUB_AUTHORIZE_URL=https://github.com/login/oauth/authorize
|
||||
GITHUB_TOKEN_URL=https://github.com/login/oauth/access_token
|
||||
|
||||
# Tapo P115 Steckdosen-Konfiguration
|
||||
TAPO_USERNAME=your_tapo_username
|
||||
TAPO_PASSWORD=your_tapo_password
|
||||
# JSON-Objekt mit der Zuordnung von Drucker-IDs zu IP-Adressen der Steckdosen
|
||||
# Beispiel: {"printer1_id": "192.168.1.100", "printer2_id": "192.168.1.101"}
|
||||
TAPO_DEVICES={}
|
49
backend/.gitignore
vendored
Normal file
49
backend/.gitignore
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
.env
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# SQLite Datenbank-Dateien
|
||||
*.db
|
||||
*.db-journal
|
||||
|
||||
# Virtuelle Umgebungen
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Betriebssystem
|
||||
.DS_Store
|
||||
Thumbs.db
|
508
backend/API_DOCS.md
Normal file
508
backend/API_DOCS.md
Normal file
@ -0,0 +1,508 @@
|
||||
# MYP Backend API-Dokumentation
|
||||
|
||||
Dieses Dokument beschreibt detailliert die API-Endpunkte des MYP (Manage Your Printer) Backend-Systems.
|
||||
|
||||
## Basis-URL
|
||||
|
||||
Die Basis-URL für alle API-Anfragen ist: `http://localhost:5000` (Entwicklungsumgebung) oder die URL, unter der die Anwendung gehostet wird.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
Die meisten Endpunkte erfordern eine Authentifizierung. Diese erfolgt über einen JWT-Token, der im Authorization-Header übermittelt wird:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Der Token wird bei erfolgreicher Anmeldung zurückgegeben und hat eine Gültigkeitsdauer von 24 Stunden.
|
||||
|
||||
### Benutzeranmeldung
|
||||
|
||||
**Endpunkt:** `POST /api/auth/login`
|
||||
|
||||
**Beschreibung:** Meldet einen Benutzer an und gibt einen JWT-Token zurück.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string",
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"token": "string",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Ungültige Anmeldedaten!"
|
||||
}
|
||||
```
|
||||
|
||||
### Benutzerregistrierung
|
||||
|
||||
**Endpunkt:** `POST /api/auth/register`
|
||||
|
||||
**Beschreibung:** Registriert einen neuen Benutzer.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Benutzer erfolgreich registriert!"
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Benutzername bereits vergeben!"
|
||||
}
|
||||
```
|
||||
|
||||
## Benutzer-Endpunkte
|
||||
|
||||
### Alle Benutzer abrufen (Admin)
|
||||
|
||||
**Endpunkt:** `GET /api/users`
|
||||
|
||||
**Beschreibung:** Gibt eine Liste aller Benutzer zurück.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Benutzer abrufen (Admin)
|
||||
|
||||
**Endpunkt:** `GET /api/users/{userId}`
|
||||
|
||||
**Beschreibung:** Gibt die Details eines bestimmten Benutzers zurück.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Nicht gefunden!"
|
||||
}
|
||||
```
|
||||
|
||||
### Benutzer aktualisieren (Admin)
|
||||
|
||||
**Endpunkt:** `PUT /api/users/{userId}`
|
||||
|
||||
**Beschreibung:** Aktualisiert die Daten eines Benutzers.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"password": "string",
|
||||
"role": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Benutzername bereits vergeben!"
|
||||
}
|
||||
```
|
||||
|
||||
### Benutzer löschen (Admin)
|
||||
|
||||
**Endpunkt:** `DELETE /api/users/{userId}`
|
||||
|
||||
**Beschreibung:** Löscht einen Benutzer.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Benutzer gelöscht!"
|
||||
}
|
||||
```
|
||||
|
||||
## Drucker-Endpunkte
|
||||
|
||||
### Alle Drucker abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/printers`
|
||||
|
||||
**Beschreibung:** Gibt eine Liste aller Drucker zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "string",
|
||||
"location": "string",
|
||||
"type": "string",
|
||||
"status": "string",
|
||||
"description": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Drucker hinzufügen (Admin)
|
||||
|
||||
**Endpunkt:** `POST /api/printers`
|
||||
|
||||
**Beschreibung:** Fügt einen neuen Drucker hinzu.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"location": "string",
|
||||
"type": "string",
|
||||
"description": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "string",
|
||||
"location": "string",
|
||||
"type": "string",
|
||||
"status": "available",
|
||||
"description": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Drucker abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/printers/{printerId}`
|
||||
|
||||
**Beschreibung:** Gibt die Details eines bestimmten Druckers zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "string",
|
||||
"location": "string",
|
||||
"type": "string",
|
||||
"status": "string",
|
||||
"description": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Nicht gefunden!"
|
||||
}
|
||||
```
|
||||
|
||||
### Drucker aktualisieren (Admin)
|
||||
|
||||
**Endpunkt:** `PUT /api/printers/{printerId}`
|
||||
|
||||
**Beschreibung:** Aktualisiert die Daten eines Druckers.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"location": "string",
|
||||
"type": "string",
|
||||
"status": "string",
|
||||
"description": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "string",
|
||||
"location": "string",
|
||||
"type": "string",
|
||||
"status": "string",
|
||||
"description": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Drucker löschen (Admin)
|
||||
|
||||
**Endpunkt:** `DELETE /api/printers/{printerId}`
|
||||
|
||||
**Beschreibung:** Löscht einen Drucker.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Drucker gelöscht!"
|
||||
}
|
||||
```
|
||||
|
||||
## Druckauftrags-Endpunkte
|
||||
|
||||
### Alle Druckaufträge abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/jobs`
|
||||
|
||||
**Beschreibung:** Gibt eine Liste aller Druckaufträge zurück (für Admins) oder der eigenen Druckaufträge (für Benutzer).
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "string",
|
||||
"start_time": "string (ISO 8601)",
|
||||
"end_time": "string (ISO 8601)",
|
||||
"duration": 60,
|
||||
"status": "string",
|
||||
"comments": "string",
|
||||
"user_id": 1,
|
||||
"printer_id": 1,
|
||||
"remaining_time": 30
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Druckauftrag erstellen
|
||||
|
||||
**Endpunkt:** `POST /api/jobs`
|
||||
|
||||
**Beschreibung:** Erstellt einen neuen Druckauftrag.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"title": "string",
|
||||
"duration": 60,
|
||||
"printer_id": 1,
|
||||
"comments": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "string",
|
||||
"start_time": "string (ISO 8601)",
|
||||
"end_time": "string (ISO 8601)",
|
||||
"duration": 60,
|
||||
"status": "active",
|
||||
"comments": "string",
|
||||
"user_id": 1,
|
||||
"printer_id": 1,
|
||||
"remaining_time": 60
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Drucker ist nicht verfügbar!"
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/jobs/{jobId}`
|
||||
|
||||
**Beschreibung:** Gibt die Details eines bestimmten Druckauftrags zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "string",
|
||||
"start_time": "string (ISO 8601)",
|
||||
"end_time": "string (ISO 8601)",
|
||||
"duration": 60,
|
||||
"status": "string",
|
||||
"comments": "string",
|
||||
"user_id": 1,
|
||||
"printer_id": 1,
|
||||
"remaining_time": 30
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Nicht gefunden!"
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag aktualisieren
|
||||
|
||||
**Endpunkt:** `PUT /api/jobs/{jobId}`
|
||||
|
||||
**Beschreibung:** Aktualisiert die Daten eines Druckauftrags.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"status": "string",
|
||||
"comments": "string",
|
||||
"duration": 30
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "string",
|
||||
"start_time": "string (ISO 8601)",
|
||||
"end_time": "string (ISO 8601)",
|
||||
"duration": 90,
|
||||
"status": "string",
|
||||
"comments": "string",
|
||||
"user_id": 1,
|
||||
"printer_id": 1,
|
||||
"remaining_time": 60
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag löschen
|
||||
|
||||
**Endpunkt:** `DELETE /api/jobs/{jobId}`
|
||||
|
||||
**Beschreibung:** Löscht einen Druckauftrag.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Druckauftrag gelöscht!"
|
||||
}
|
||||
```
|
||||
|
||||
### Verbleibende Zeit eines Druckauftrags abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/job/{jobId}/remaining-time`
|
||||
|
||||
**Beschreibung:** Gibt die verbleibende Zeit eines aktiven Druckauftrags in Minuten zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"remaining_minutes": 30
|
||||
}
|
||||
```
|
||||
|
||||
## Statistik-Endpunkte
|
||||
|
||||
### Systemstatistiken abrufen (Admin)
|
||||
|
||||
**Endpunkt:** `GET /api/stats`
|
||||
|
||||
**Beschreibung:** Gibt Statistiken zu Druckern, Aufträgen und Benutzern zurück.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"printers": {
|
||||
"total": 10,
|
||||
"available": 5,
|
||||
"utilization_rate": 0.5
|
||||
},
|
||||
"jobs": {
|
||||
"total": 100,
|
||||
"active": 5,
|
||||
"completed": 90,
|
||||
"avg_duration": 120
|
||||
},
|
||||
"users": {
|
||||
"total": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test-Endpunkt
|
||||
|
||||
### API-Test
|
||||
|
||||
**Endpunkt:** `GET /api/test`
|
||||
|
||||
**Beschreibung:** Testet, ob die API funktioniert.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "MYP Backend API funktioniert!"
|
||||
}
|
||||
```
|
||||
|
||||
## Fehlercodes
|
||||
|
||||
| Statuscode | Beschreibung |
|
||||
|------------|-----------------------------|
|
||||
| 200 | OK - Anfrage erfolgreich |
|
||||
| 201 | Created - Ressource erstellt |
|
||||
| 400 | Bad Request - Ungültige Anfrage |
|
||||
| 401 | Unauthorized - Authentifizierung erforderlich |
|
||||
| 403 | Forbidden - Unzureichende Rechte |
|
||||
| 404 | Not Found - Ressource nicht gefunden |
|
||||
| 500 | Internal Server Error - Serverfehler |
|
18
backend/Dockerfile
Normal file
18
backend/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p logs
|
||||
|
||||
ENV FLASK_APP=app.py
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
213
backend/PROJEKTDOKUMENTATION.md
Normal file
213
backend/PROJEKTDOKUMENTATION.md
Normal file
@ -0,0 +1,213 @@
|
||||
# MYP - Projektdokumentation für das IHK-Abschlussprojekt
|
||||
|
||||
## Projektübersicht
|
||||
|
||||
**Projektname:** MYP (Manage Your Printer)
|
||||
**Projekttyp:** IHK-Abschlussprojekt für Fachinformatiker für digitale Vernetzung
|
||||
**Zeitraum:** [Dein Projektzeitraum]
|
||||
**Team:** 2 Personen (Frontend- und Backend-Entwicklung)
|
||||
|
||||
## Projektziel
|
||||
|
||||
Das Ziel des Projektes ist die Entwicklung einer Reservierungs- und Steuerungsplattform für 3D-Drucker, die es Benutzern ermöglicht, Drucker zu reservieren und deren Stromversorgung automatisch über WLAN-Steckdosen (Tapo P115) zu steuern. Die Plattform soll eine einfache Verwaltung der Drucker und ihrer Auslastung bieten sowie den Stromverbrauch optimieren, indem Drucker nur während aktiver Reservierungen mit Strom versorgt werden.
|
||||
|
||||
## Aufgabenbeschreibung
|
||||
|
||||
Als Fachinformatiker für digitale Vernetzung besteht meine Aufgabe in der Entwicklung des Backend-Systems, das folgende Funktionen bereitstellt:
|
||||
|
||||
1. **API-Backend für das Frontend**: Entwicklung einer RESTful API, die mit dem Frontend kommuniziert und alle notwendigen Daten bereitstellt.
|
||||
|
||||
2. **Authentifizierungssystem**: Integration einer OAuth-Authentifizierung über GitHub, um Benutzer zu identifizieren und Zugriffskontrolle zu gewährleisten.
|
||||
|
||||
3. **Datenbankverwaltung**: Erstellung und Verwaltung der Datenbankmodelle für Benutzer, Drucker und Reservierungen.
|
||||
|
||||
4. **Steckdosensteuerung**: Implementierung einer Schnittstelle zu Tapo P115 WLAN-Steckdosen, um die Stromversorgung der Drucker basierend auf Reservierungen zu steuern.
|
||||
|
||||
5. **Automatisierung**: Entwicklung von Mechanismen zur automatischen Überwachung von Reservierungen und Steuerung der Steckdosen.
|
||||
|
||||
6. **Sicherheit**: Implementierung von Sicherheitsmaßnahmen zum Schutz der Anwendung und der Daten.
|
||||
|
||||
7. **Dokumentation**: Erstellung einer umfassenden Dokumentation für Entwicklung, Installation und Nutzung des Systems.
|
||||
|
||||
## Technische Umsetzung
|
||||
|
||||
### Backend (Mein Verantwortungsbereich)
|
||||
|
||||
#### Verwendete Technologien
|
||||
|
||||
- **Programmiersprache**: Python 3.11
|
||||
- **Web-Framework**: Flask 2.3.3
|
||||
- **Datenbank-ORM**: SQLAlchemy 3.1.1
|
||||
- **Datenbank**: SQLite (für Entwicklung), erweiterbar auf PostgreSQL für Produktion
|
||||
- **Authentifizierung**: Authlib für GitHub OAuth
|
||||
- **Steckdosen-Steuerung**: Tapo Python Library
|
||||
- **Container-Technologie**: Docker und Docker Compose
|
||||
|
||||
#### Architektur
|
||||
|
||||
Die Backend-Anwendung folgt einer klassischen dreischichtigen Architektur:
|
||||
|
||||
1. **Datenmodell-Schicht**: SQLAlchemy ORM-Modelle für Benutzer, Sessions, Drucker und Druckaufträge
|
||||
2. **Business-Logic-Schicht**: Implementierung der Geschäftslogik für Reservierungsverwaltung und Steckdosensteuerung
|
||||
3. **API-Schicht**: RESTful API-Endpunkte, die vom Frontend konsumiert werden
|
||||
|
||||
Zusätzlich wurden folgende Features implementiert:
|
||||
|
||||
- **OAuth-Authentifizierung**: Implementierung einer sicheren Authentifizierung über GitHub
|
||||
- **Session-Management**: Server-seitige Session-Verwaltung für Benutzerauthentifizierung
|
||||
- **Steckdosensteuerung**: Asynchrone Steuerung der Tapo P115 WLAN-Steckdosen
|
||||
- **CLI-Befehle**: Flask CLI-Befehle für automatisierte Aufgaben wie die Überprüfung abgelaufener Reservierungen
|
||||
|
||||
#### Datenmodell
|
||||
|
||||
Das Datenmodell besteht aus vier Hauptentitäten:
|
||||
|
||||
1. **User**: Benutzer mit GitHub-Authentifizierung und Rollenverwaltung
|
||||
2. **Session**: Sitzungsdaten für die Authentifizierung
|
||||
3. **Printer**: Drucker mit Status und IP-Adresse der zugehörigen Steckdose
|
||||
4. **PrintJob**: Reservierungen mit Start- und Endzeit, Dauer und Status
|
||||
|
||||
#### API-Endpunkte
|
||||
|
||||
Die API wurde speziell entwickelt, um nahtlos mit dem bestehenden Frontend zusammenzuarbeiten. Sie bietet Endpunkte für:
|
||||
|
||||
- Authentifizierung und Benutzerverwaltung
|
||||
- Druckerverwaltung
|
||||
- Reservierungsverwaltung (Erstellen, Abbrechen, Verlängern)
|
||||
- Statusinformationen wie verbleibende Zeit
|
||||
|
||||
#### Steckdosensteuerung
|
||||
|
||||
Die Steuerung der Tapo P115 WLAN-Steckdosen erfolgt über die Tapo Python Library. Das System:
|
||||
|
||||
- Schaltet Steckdosen bei Erstellung einer Reservierung ein
|
||||
- Schaltet Steckdosen bei Abbruch oder Beendigung einer Reservierung aus
|
||||
- Überprüft regelmäßig abgelaufene Reservierungen und schaltet die entsprechenden Steckdosen aus
|
||||
|
||||
#### Automatisierung
|
||||
|
||||
Das System implementiert mehrere Automatisierungsmechanismen:
|
||||
|
||||
- **Automatische Steckdosensteuerung**: Ein- und Ausschalten der Steckdosen basierend auf Reservierungsstatus
|
||||
- **Job-Überprüfung**: CLI-Befehl `flask check-jobs` zur regelmäßigen Überprüfung abgelaufener Reservierungen
|
||||
- **Logging**: Automatische Protokollierung aller Aktionen zur Fehlerdiagnose
|
||||
|
||||
### Frontend (Verantwortungsbereich des Teampartners)
|
||||
|
||||
Das Frontend wurde von meinem Teampartner entwickelt und besteht aus:
|
||||
|
||||
- Next.js-Anwendung mit React-Komponenten
|
||||
- Tailwind CSS für das Styling
|
||||
- Serverless Functions für API-Integrationen
|
||||
- Responsive Design für Desktop- und Mobile-Nutzung
|
||||
|
||||
## Projektergebnisse
|
||||
|
||||
Das Projekt hat erfolgreich eine funktionsfähige Reservierungs- und Steuerungsplattform für 3D-Drucker geschaffen, die es Benutzern ermöglicht:
|
||||
|
||||
1. Sich über GitHub zu authentifizieren
|
||||
2. Verfügbare Drucker zu sehen und zu reservieren
|
||||
3. Ihre Reservierungen zu verwalten (verlängern, abbrechen, kommentieren)
|
||||
4. Als Administrator Drucker und Benutzer zu verwalten
|
||||
|
||||
Technische Errungenschaften:
|
||||
|
||||
1. Nahtlose Integration mit dem Frontend
|
||||
2. Erfolgreiche Implementierung der Steckdosensteuerung
|
||||
3. Sichere Authentifizierung über GitHub OAuth
|
||||
4. Optimierte Stromnutzung durch automatische Steckdosensteuerung
|
||||
|
||||
## Herausforderungen und Lösungen
|
||||
|
||||
### Herausforderung 1: GitHub OAuth-Integration
|
||||
|
||||
Die Integration der GitHub-Authentifizierung, insbesondere mit GitHub Enterprise, erforderte eine sorgfältige Konfiguration der OAuth-Einstellungen und URL-Anpassungen.
|
||||
|
||||
**Lösung:** Implementierung mit Authlib und anpassbaren Konfigurationsoptionen für verschiedene GitHub-Instanzen.
|
||||
|
||||
### Herausforderung 2: Tapo P115 Steuerung
|
||||
|
||||
Die Kommunikation mit den Tapo P115 WLAN-Steckdosen erforderte eine zuverlässige und asynchrone Implementierung.
|
||||
|
||||
**Lösung:** Verwendung der Tapo Python Library mit asynchronem Handling und robusten Fehlerbehandlungsmechanismen.
|
||||
|
||||
### Herausforderung 3: Kompatibilität mit bestehendem Frontend
|
||||
|
||||
Das Backend musste mit dem bereits entwickelten Frontend kompatibel sein, was eine genaue Anpassung der API-Endpunkte und Datenstrukturen erforderte.
|
||||
|
||||
**Lösung:** Sorgfältige Analyse des Frontend-Codes, um die erwarteten API-Strukturen zu verstehen und das Backend entsprechend zu implementieren.
|
||||
|
||||
### Herausforderung 4: Automatische Steckdosensteuerung
|
||||
|
||||
Die zuverlässige Steuerung der Steckdosen bei abgelaufenen Reservierungen war eine Herausforderung.
|
||||
|
||||
**Lösung:** Implementierung eines CLI-Befehls, der regelmäßig durch Cron-Jobs ausgeführt werden kann, um abgelaufene Reservierungen zu überprüfen.
|
||||
|
||||
## Fachliche Reflexion
|
||||
|
||||
Das Projekt erforderte ein breites Spektrum an Fähigkeiten aus dem Bereich der digitalen Vernetzung:
|
||||
|
||||
1. **Netzwerkkommunikation**: Implementierung der Kommunikation zwischen Backend, Frontend und WLAN-Steckdosen über verschiedene Protokolle.
|
||||
|
||||
2. **Systemintegration**: Integration verschiedener Systeme (GitHub OAuth, Datenbank, Tapo-Steckdosen) zu einer kohärenten Anwendung.
|
||||
|
||||
3. **API-Design**: Entwicklung einer RESTful API, die den Anforderungen des Frontends entspricht und zukunftssicher ist.
|
||||
|
||||
4. **Datenbankentwurf**: Erstellung eines optimierten Datenbankschemas für die Anwendung.
|
||||
|
||||
5. **Sicherheitskonzepte**: Implementierung von Sicherheitsmaßnahmen wie OAuth, Session-Management und Zugriffskontrollen.
|
||||
|
||||
6. **Automatisierung**: Entwicklung von Automatisierungsprozessen für die Steckdosensteuerung und Job-Überwachung.
|
||||
|
||||
Diese Aspekte entsprechen direkt den Kernkompetenzen des Berufsbildes "Fachinformatiker für digitale Vernetzung" und zeigen die praktische Anwendung dieser Fähigkeiten in einem realen Projekt.
|
||||
|
||||
## Ausblick und Weiterentwicklung
|
||||
|
||||
Das System bietet verschiedene Möglichkeiten zur Weiterentwicklung:
|
||||
|
||||
1. **Erweiterung der Steckdosenunterstützung**: Integration weiterer Smart-Home-Geräte neben Tapo P115.
|
||||
|
||||
2. **Benachrichtigungssystem**: Implementierung von E-Mail- oder Push-Benachrichtigungen für Reservierungserinnerungen.
|
||||
|
||||
3. **Erweiterte Statistiken**: Detailliertere Nutzungsstatistiken und Visualisierungen für Administratoren.
|
||||
|
||||
4. **Mobile App**: Entwicklung einer nativen mobilen App für iOS und Android.
|
||||
|
||||
5. **Verbesserte Automatisierung**: Integration mit weiteren Systemen wie 3D-Drucker-APIs für direktes Monitoring des Druckstatus.
|
||||
|
||||
## Fazit
|
||||
|
||||
Das MYP-Projekt zeigt erfolgreich, wie moderne Webtechnologien und IoT-Geräte kombiniert werden können, um eine praktische Lösung für die Verwaltung von 3D-Druckern zu schaffen.
|
||||
|
||||
Als angehender Fachinformatiker für digitale Vernetzung konnte ich meine Fähigkeiten in den Bereichen Programmierung, Systemintegration, Netzwerkkommunikation und Automatisierung anwenden und erweitern.
|
||||
|
||||
Die Zusammenarbeit im Team mit klarer Aufgabenteilung (Frontend/Backend) hat zu einem erfolgreichen Projektergebnis geführt, das die gestellten Anforderungen erfüllt und einen praktischen Nutzen bietet.
|
||||
|
||||
---
|
||||
|
||||
## Anhang
|
||||
|
||||
### Installation und Einrichtung
|
||||
|
||||
Detaillierte Anweisungen zur Installation und Einrichtung des Backend-Systems finden sich in der README.md-Datei.
|
||||
|
||||
### Wichtige Konfigurationsparameter
|
||||
|
||||
Die folgenden Umgebungsvariablen müssen konfiguriert werden:
|
||||
|
||||
- `SECRET_KEY`: Geheimer Schlüssel für die Session-Verschlüsselung
|
||||
- `DATABASE_URL`: URL zur Datenbank
|
||||
- `OAUTH_CLIENT_ID`: GitHub OAuth Client ID
|
||||
- `OAUTH_CLIENT_SECRET`: GitHub OAuth Client Secret
|
||||
- `GITHUB_API_BASE_URL`, `GITHUB_AUTHORIZE_URL`, `GITHUB_TOKEN_URL`: URLs für GitHub OAuth
|
||||
- `TAPO_USERNAME`: Benutzername für die Tapo-Steckdosen
|
||||
- `TAPO_PASSWORD`: Passwort für die Tapo-Steckdosen
|
||||
- `TAPO_DEVICES`: JSON-Objekt mit der Zuordnung von Drucker-IDs zu IP-Adressen
|
||||
|
||||
### Cron-Job-Einrichtung
|
||||
|
||||
Für die automatische Überprüfung abgelaufener Jobs kann folgender Cron-Job eingerichtet werden:
|
||||
|
||||
```
|
||||
*/5 * * * * cd /pfad/zum/projekt && /pfad/zur/venv/bin/flask check-jobs >> /pfad/zum/projekt/logs/cron.log 2>&1
|
||||
```
|
188
backend/README.md
Normal file
188
backend/README.md
Normal file
@ -0,0 +1,188 @@
|
||||
# MYP Backend-Steuerungsplattform
|
||||
|
||||
Dies ist das Backend für das MYP (Manage Your Printer) Projekt, ein IHK-Abschlussprojekt für Fachinformatiker für digitale Vernetzung. Die Plattform ist mit Python und Flask implementiert und stellt eine RESTful API zur Verfügung, die es ermöglicht, 3D-Drucker zu verwalten, zu reservieren und über WLAN-Steckdosen (Tapo P115) zu steuern.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- OAuth-Authentifizierung mit GitHub
|
||||
- Rollen-basierte Zugriffskontrolle (Admin/User/Guest)
|
||||
- Druckerverwaltung (Hinzufügen, Bearbeiten, Löschen)
|
||||
- Reservierungsverwaltung (Erstellen, Abbrechen, Verlängern)
|
||||
- Fernsteuerung von WLAN-Steckdosen (Tapo P115)
|
||||
- Statistikerfassung und -anzeige
|
||||
- RESTful API für die Kommunikation mit dem Frontend
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- **Python**: Programmiersprache
|
||||
- **Flask**: Web-Framework
|
||||
- **SQLAlchemy**: ORM für Datenbankinteraktionen
|
||||
- **SQLite**: Datenbank (kann für Produktion durch PostgreSQL ersetzt werden)
|
||||
- **Authlib**: OAuth-Integration für GitHub-Authentifizierung
|
||||
- **Tapo Python Library**: Steuerung der Tapo P115 WLAN-Steckdosen
|
||||
- **Gunicorn**: WSGI HTTP Server für die Produktionsumgebung
|
||||
- **Docker**: Containerisierung der Anwendung
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
- `app.py`: Hauptanwendungsdatei mit allen Routen und Modellen
|
||||
- `requirements.txt`: Liste aller Python-Abhängigkeiten
|
||||
- `Dockerfile`: Docker-Konfiguration
|
||||
- `docker-compose.yml`: Docker Compose Konfiguration für einfaches Deployment
|
||||
- `.env.example`: Beispiel für die Umgebungsvariablen
|
||||
- `logs/`: Logdateien (automatisch erstellt)
|
||||
- `instance/`: SQLite-Datenbank (automatisch erstellt)
|
||||
|
||||
## Installation und Ausführung
|
||||
|
||||
### Lokal (Entwicklung)
|
||||
|
||||
1. Python 3.8 oder höher installieren
|
||||
2. Repository klonen
|
||||
3. Ins Projektverzeichnis wechseln
|
||||
4. Virtuelle Umgebung erstellen (optional, aber empfohlen)
|
||||
```
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Unter Windows: venv\Scripts\activate
|
||||
```
|
||||
5. Abhängigkeiten installieren
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
6. `.env.example` nach `.env` kopieren und anpassen
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
7. Anwendung starten
|
||||
```
|
||||
python app.py
|
||||
```
|
||||
|
||||
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
||||
|
||||
### Mit Docker
|
||||
|
||||
1. Docker und Docker Compose installieren
|
||||
2. Ins Projektverzeichnis wechseln
|
||||
3. `.env.example` nach `.env` kopieren und anpassen
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
4. Anwendung starten
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### Authentifizierung
|
||||
|
||||
- `GET /auth/login`: GitHub-Anmeldung einleiten
|
||||
- `GET /auth/login/callback`: GitHub-Callback-Endpunkt
|
||||
- `POST /auth/logout`: Abmelden und Session beenden
|
||||
- `GET /api/me`: Aktuelle Benutzerinformationen abrufen
|
||||
|
||||
### Benutzer
|
||||
|
||||
- `GET /api/users`: Liste aller Benutzer (Admin)
|
||||
- `GET /api/users/<id>`: Details zu einem Benutzer (Admin)
|
||||
- `PUT /api/users/<id>`: Benutzer aktualisieren (Admin)
|
||||
- `DELETE /api/users/<id>`: Benutzer löschen (Admin)
|
||||
|
||||
### Drucker
|
||||
|
||||
- `GET /api/printers`: Liste aller Drucker
|
||||
- `POST /api/printers`: Drucker hinzufügen (Admin)
|
||||
- `GET /api/printers/<id>`: Details zu einem Drucker
|
||||
- `PUT /api/printers/<id>`: Drucker aktualisieren (Admin)
|
||||
- `DELETE /api/printers/<id>`: Drucker löschen (Admin)
|
||||
|
||||
### Druckaufträge
|
||||
|
||||
- `GET /api/jobs`: Liste aller Druckaufträge (Admin) oder eigener Druckaufträge (Benutzer)
|
||||
- `POST /api/jobs`: Druckauftrag erstellen
|
||||
- `GET /api/jobs/<id>`: Details zu einem Druckauftrag
|
||||
- `POST /api/jobs/<id>/abort`: Druckauftrag abbrechen
|
||||
- `POST /api/jobs/<id>/finish`: Druckauftrag vorzeitig beenden
|
||||
- `POST /api/jobs/<id>/extend`: Druckauftrag verlängern
|
||||
- `PUT /api/jobs/<id>/comments`: Kommentare aktualisieren
|
||||
- `GET /api/job/<id>/remaining-time`: Verbleibende Zeit für einen aktiven Druckauftrag
|
||||
|
||||
### Statistiken
|
||||
|
||||
- `GET /api/stats`: Statistiken zu Druckern, Aufträgen und Benutzern (Admin)
|
||||
|
||||
## Datenmodell
|
||||
|
||||
### Benutzer (User)
|
||||
- id (String UUID, Primary Key)
|
||||
- github_id (Integer, Unique)
|
||||
- username (String, Unique)
|
||||
- display_name (String)
|
||||
- email (String, Unique)
|
||||
- role (String, 'admin', 'user' oder 'guest')
|
||||
|
||||
### Session
|
||||
- id (String UUID, Primary Key)
|
||||
- user_id (String UUID, Foreign Key zu User)
|
||||
- expires_at (DateTime)
|
||||
|
||||
### Drucker (Printer)
|
||||
- id (String UUID, Primary Key)
|
||||
- name (String)
|
||||
- description (Text)
|
||||
- status (Integer, 0=available, 1=busy, 2=maintenance)
|
||||
- ip_address (String, IP-Adresse der Tapo-Steckdose)
|
||||
|
||||
### Druckauftrag (PrintJob)
|
||||
- id (String UUID, Primary Key)
|
||||
- printer_id (String UUID, Foreign Key zu Printer)
|
||||
- user_id (String UUID, Foreign Key zu User)
|
||||
- start_at (DateTime)
|
||||
- duration_in_minutes (Integer)
|
||||
- comments (Text)
|
||||
- aborted (Boolean)
|
||||
- abort_reason (Text)
|
||||
|
||||
## Steckdosensteuerung
|
||||
|
||||
Die Anwendung steuert Tapo P115 WLAN-Steckdosen, um die Drucker basierend auf Reservierungen ein- und auszuschalten:
|
||||
|
||||
- Bei Erstellung eines Druckauftrags wird die Steckdose des zugehörigen Druckers automatisch eingeschaltet
|
||||
- Bei Abbruch oder vorzeitiger Beendigung eines Druckauftrags wird die Steckdose ausgeschaltet
|
||||
- Nach Ablauf der Reservierungszeit wird die Steckdose automatisch ausgeschaltet
|
||||
- Ein CLI-Befehl `flask check-jobs` überprüft regelmäßig abgelaufene Jobs und schaltet Steckdosen aus
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- Die Anwendung verwendet GitHub OAuth für die Authentifizierung
|
||||
- Sitzungsdaten werden in Server-Side-Sessions gespeichert
|
||||
- Zugriffskontrollen sind implementiert, um sicherzustellen, dass Benutzer nur auf ihre eigenen Daten zugreifen können
|
||||
- Admin-Benutzer haben Zugriff auf alle Daten und können Systemkonfigurationen ändern
|
||||
|
||||
## Logging
|
||||
|
||||
Die Anwendung protokolliert Aktivitäten in rotierenden Logdateien in einem `logs` Verzeichnis. Dies hilft bei der Fehlersuche und Überwachung der Anwendung im Betrieb.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
Die folgenden Umgebungsvariablen müssen konfiguriert werden:
|
||||
|
||||
- `SECRET_KEY`: Geheimer Schlüssel für die Session-Verschlüsselung
|
||||
- `DATABASE_URL`: URL zur Datenbank (Standard: SQLite-Datenbank im Instance-Verzeichnis)
|
||||
- `OAUTH_CLIENT_ID`: GitHub OAuth Client ID
|
||||
- `OAUTH_CLIENT_SECRET`: GitHub OAuth Client Secret
|
||||
- `GITHUB_API_BASE_URL`, `GITHUB_AUTHORIZE_URL`, `GITHUB_TOKEN_URL`: URLs für GitHub OAuth (falls GitHub Enterprise verwendet wird)
|
||||
- `TAPO_USERNAME`: Benutzername für die Tapo-Steckdosen
|
||||
- `TAPO_PASSWORD`: Passwort für die Tapo-Steckdosen
|
||||
- `TAPO_DEVICES`: JSON-Objekt mit der Zuordnung von Drucker-IDs zu IP-Adressen der Steckdosen
|
||||
|
||||
## Automatisierung
|
||||
|
||||
Die Anwendung beinhaltet einen CLI-Befehl `flask check-jobs`, der regelmäßig ausgeführt werden sollte (z.B. als Cron-Job), um abgelaufene Druckaufträge zu überprüfen und die zugehörigen Steckdosen auszuschalten.
|
||||
|
||||
## Kompatibilität mit dem Frontend
|
||||
|
||||
Das Backend wurde speziell für die Kompatibilität mit dem bestehenden Frontend entwickelt, welches in `/packages/reservation-platform` zu finden ist. Die API-Endpunkte und Datenstrukturen sind so gestaltet, dass sie nahtlos mit dem Frontend zusammenarbeiten.
|
696
backend/app.py
Normal file
696
backend/app.py
Normal file
@ -0,0 +1,696 @@
|
||||
from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from functools import wraps
|
||||
import jwt
|
||||
import datetime
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import requests
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from datetime import timedelta
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
import sqlite3
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
from tapo import ApiClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Lade Umgebungsvariablen
|
||||
load_dotenv()
|
||||
|
||||
# Initialisierung
|
||||
app = Flask(__name__)
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
# Konfiguration
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_secret_key')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///myp.db')
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SECURE'] = os.environ.get('FLASK_ENV') == 'production'
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
||||
|
||||
# GitHub OAuth Konfiguration
|
||||
app.config['GITHUB_CLIENT_ID'] = os.environ.get('OAUTH_CLIENT_ID')
|
||||
app.config['GITHUB_CLIENT_SECRET'] = os.environ.get('OAUTH_CLIENT_SECRET')
|
||||
app.config['GITHUB_API_BASE_URL'] = os.environ.get('GITHUB_API_BASE_URL', 'https://api.github.com/')
|
||||
app.config['GITHUB_AUTHORIZE_URL'] = os.environ.get('GITHUB_AUTHORIZE_URL', 'https://github.com/login/oauth/authorize')
|
||||
app.config['GITHUB_TOKEN_URL'] = os.environ.get('GITHUB_TOKEN_URL', 'https://github.com/login/oauth/access_token')
|
||||
|
||||
# Tapo-Konfiguration
|
||||
TAPO_USERNAME = os.environ.get('TAPO_USERNAME')
|
||||
TAPO_PASSWORD = os.environ.get('TAPO_PASSWORD')
|
||||
TAPO_DEVICES = json.loads(os.environ.get('TAPO_DEVICES', '{}'))
|
||||
|
||||
# Logging
|
||||
if not os.path.exists('logs'):
|
||||
os.mkdir('logs')
|
||||
file_handler = RotatingFileHandler('logs/myp.log', maxBytes=10240, backupCount=10)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||
))
|
||||
file_handler.setLevel(logging.INFO)
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.info('MYP Backend starting up')
|
||||
|
||||
# DB Setup
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
# OAuth Setup
|
||||
oauth = OAuth(app)
|
||||
github = oauth.register(
|
||||
name='github',
|
||||
client_id=app.config['GITHUB_CLIENT_ID'],
|
||||
client_secret=app.config['GITHUB_CLIENT_SECRET'],
|
||||
access_token_url=app.config['GITHUB_TOKEN_URL'],
|
||||
authorize_url=app.config['GITHUB_AUTHORIZE_URL'],
|
||||
api_base_url=app.config['GITHUB_API_BASE_URL'],
|
||||
client_kwargs={'scope': 'user:email'},
|
||||
)
|
||||
|
||||
# Models
|
||||
class User(db.Model):
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
github_id = db.Column(db.Integer, unique=True)
|
||||
username = db.Column(db.String(64), index=True, unique=True)
|
||||
display_name = db.Column(db.String(100))
|
||||
email = db.Column(db.String(120), index=True, unique=True)
|
||||
role = db.Column(db.String(20), default='guest') # admin, user, guest
|
||||
jobs = db.relationship('PrintJob', backref='user', lazy='dynamic')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'github_id': self.github_id,
|
||||
'username': self.username,
|
||||
'displayName': self.display_name,
|
||||
'email': self.email,
|
||||
'role': self.role
|
||||
}
|
||||
|
||||
class Session(db.Model):
|
||||
id = db.Column(db.String(36), primary_key=True)
|
||||
user_id = db.Column(db.String(36), db.ForeignKey('user.id'), nullable=False)
|
||||
expires_at = db.Column(db.DateTime, nullable=False)
|
||||
user = db.relationship('User', backref=db.backref('sessions', lazy=True))
|
||||
|
||||
class Printer(db.Model):
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name = db.Column(db.String(64), index=True, nullable=False)
|
||||
description = db.Column(db.Text, nullable=False)
|
||||
status = db.Column(db.Integer, default=0) # 0=available, 1=busy, 2=maintenance
|
||||
ip_address = db.Column(db.String(15), nullable=True) # IP-Adresse der Tapo-Steckdose
|
||||
jobs = db.relationship('PrintJob', backref='printer', lazy='dynamic')
|
||||
|
||||
def get_latest_job(self):
|
||||
return PrintJob.query.filter_by(printer_id=self.id).order_by(PrintJob.start_at.desc()).first()
|
||||
|
||||
def to_dict(self):
|
||||
latest_job = self.get_latest_job()
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'status': self.status,
|
||||
'latestJob': latest_job.to_dict() if latest_job else None
|
||||
}
|
||||
|
||||
class PrintJob(db.Model):
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
printer_id = db.Column(db.String(36), db.ForeignKey('printer.id'), nullable=False)
|
||||
user_id = db.Column(db.String(36), db.ForeignKey('user.id'), nullable=False)
|
||||
start_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
duration_in_minutes = db.Column(db.Integer, nullable=False)
|
||||
comments = db.Column(db.Text, nullable=True)
|
||||
aborted = db.Column(db.Boolean, default=False)
|
||||
abort_reason = db.Column(db.Text, nullable=True)
|
||||
|
||||
@property
|
||||
def end_at(self):
|
||||
return self.start_at + timedelta(minutes=self.duration_in_minutes)
|
||||
|
||||
def remaining_time(self):
|
||||
if self.aborted:
|
||||
return 0
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
if now > self.end_at:
|
||||
return 0
|
||||
|
||||
diff = self.end_at - now
|
||||
return int(diff.total_seconds() / 60)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'printerId': self.printer_id,
|
||||
'userId': self.user_id,
|
||||
'startAt': self.start_at.isoformat(),
|
||||
'durationInMinutes': self.duration_in_minutes,
|
||||
'comments': self.comments,
|
||||
'aborted': self.aborted,
|
||||
'abortReason': self.abort_reason,
|
||||
'remainingMinutes': self.remaining_time()
|
||||
}
|
||||
|
||||
# Tapo Steckdosen-Steuerung
|
||||
class TapoControl:
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.clients = {}
|
||||
|
||||
async def get_client(self, ip_address):
|
||||
if ip_address not in self.clients:
|
||||
try:
|
||||
client = ApiClient(self.username, self.password)
|
||||
await client.login()
|
||||
self.clients[ip_address] = client
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler bei der Anmeldung an Tapo-Gerät {ip_address}: {e}")
|
||||
return None
|
||||
return self.clients[ip_address]
|
||||
|
||||
async def turn_on(self, ip_address):
|
||||
client = await self.get_client(ip_address)
|
||||
if client:
|
||||
try:
|
||||
device = await client.p115(ip_address)
|
||||
await device.on()
|
||||
app.logger.info(f"Tapo-Steckdose {ip_address} eingeschaltet")
|
||||
return True
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler beim Einschalten der Tapo-Steckdose {ip_address}: {e}")
|
||||
return False
|
||||
|
||||
async def turn_off(self, ip_address):
|
||||
client = await self.get_client(ip_address)
|
||||
if client:
|
||||
try:
|
||||
device = await client.p115(ip_address)
|
||||
await device.off()
|
||||
app.logger.info(f"Tapo-Steckdose {ip_address} ausgeschaltet")
|
||||
return True
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler beim Ausschalten der Tapo-Steckdose {ip_address}: {e}")
|
||||
return False
|
||||
|
||||
tapo_control = TapoControl(TAPO_USERNAME, TAPO_PASSWORD)
|
||||
|
||||
# Hilfsfunktionen
|
||||
def get_current_user():
|
||||
session_id = flask_session.get('session_id')
|
||||
if not session_id:
|
||||
return None
|
||||
|
||||
session = Session.query.get(session_id)
|
||||
if not session or session.expires_at < datetime.datetime.utcnow():
|
||||
if session:
|
||||
db.session.delete(session)
|
||||
db.session.commit()
|
||||
flask_session.pop('session_id', None)
|
||||
return None
|
||||
|
||||
return session.user
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'message': 'Authentifizierung erforderlich!'}), 401
|
||||
|
||||
g.current_user = user
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not g.current_user or g.current_user.role != 'admin':
|
||||
return jsonify({'message': 'Admin-Rechte erforderlich!'}), 403
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
def create_session(user):
|
||||
session_id = str(uuid.uuid4())
|
||||
expires_at = datetime.datetime.utcnow() + timedelta(days=7)
|
||||
|
||||
session = Session(id=session_id, user_id=user.id, expires_at=expires_at)
|
||||
db.session.add(session)
|
||||
db.session.commit()
|
||||
|
||||
flask_session['session_id'] = session_id
|
||||
flask_session.permanent = True
|
||||
|
||||
return session_id
|
||||
|
||||
# Authentifizierungs-Routen
|
||||
@app.route('/auth/login', methods=['GET'])
|
||||
def login():
|
||||
redirect_uri = url_for('github_callback', _external=True)
|
||||
return github.authorize_redirect(redirect_uri)
|
||||
|
||||
@app.route('/auth/login/callback', methods=['GET'])
|
||||
def github_callback():
|
||||
token = github.authorize_access_token()
|
||||
resp = github.get('user', token=token)
|
||||
github_user = resp.json()
|
||||
|
||||
# GitHub-User-Informationen
|
||||
github_id = github_user['id']
|
||||
username = github_user['login']
|
||||
display_name = github_user.get('name', username)
|
||||
|
||||
# E-Mail abrufen
|
||||
emails_resp = github.get('user/emails', token=token)
|
||||
emails = emails_resp.json()
|
||||
primary_email = next((email['email'] for email in emails if email.get('primary')), None)
|
||||
|
||||
if not primary_email and emails:
|
||||
primary_email = emails[0]['email']
|
||||
|
||||
# Benutzer suchen oder erstellen
|
||||
user = User.query.filter_by(github_id=github_id).first()
|
||||
|
||||
if not user:
|
||||
user = User(
|
||||
github_id=github_id,
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
email=primary_email,
|
||||
role='guest' # Standardrolle für neue Benutzer
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
app.logger.info(f'Neuer Benutzer über GitHub registriert: {username}')
|
||||
else:
|
||||
# Aktualisiere Benutzerdaten, falls sie sich geändert haben
|
||||
user.username = username
|
||||
user.display_name = display_name
|
||||
if primary_email:
|
||||
user.email = primary_email
|
||||
db.session.commit()
|
||||
|
||||
# Session erstellen
|
||||
create_session(user)
|
||||
|
||||
# Weiterleitung zur Frontend-App
|
||||
return redirect('/')
|
||||
|
||||
@app.route('/auth/logout', methods=['POST'])
|
||||
def logout():
|
||||
session_id = flask_session.get('session_id')
|
||||
if session_id:
|
||||
session = Session.query.get(session_id)
|
||||
if session:
|
||||
db.session.delete(session)
|
||||
db.session.commit()
|
||||
|
||||
flask_session.pop('session_id', None)
|
||||
|
||||
return jsonify({'message': 'Erfolgreich abgemeldet!'}), 200
|
||||
|
||||
# API-Routen
|
||||
@app.route('/api/me', methods=['GET'])
|
||||
def get_me():
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'authenticated': False}), 401
|
||||
|
||||
return jsonify({
|
||||
'authenticated': True,
|
||||
'user': user.to_dict()
|
||||
})
|
||||
|
||||
@app.route('/api/printers', methods=['GET'])
|
||||
def get_printers():
|
||||
printers = Printer.query.all()
|
||||
return jsonify([printer.to_dict() for printer in printers])
|
||||
|
||||
@app.route('/api/printers', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def create_printer():
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('name') or not data.get('description'):
|
||||
return jsonify({'message': 'Name und Beschreibung sind erforderlich!'}), 400
|
||||
|
||||
printer = Printer(
|
||||
name=data.get('name'),
|
||||
description=data.get('description'),
|
||||
status=data.get('status', 0),
|
||||
ip_address=data.get('ipAddress')
|
||||
)
|
||||
|
||||
db.session.add(printer)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(printer.to_dict()), 201
|
||||
|
||||
@app.route('/api/printers/<printer_id>', methods=['GET'])
|
||||
def get_printer(printer_id):
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
return jsonify(printer.to_dict())
|
||||
|
||||
@app.route('/api/printers/<printer_id>', methods=['PUT'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def update_printer(printer_id):
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
data = request.get_json()
|
||||
|
||||
if data.get('name'):
|
||||
printer.name = data['name']
|
||||
if data.get('description'):
|
||||
printer.description = data['description']
|
||||
if 'status' in data:
|
||||
printer.status = data['status']
|
||||
if data.get('ipAddress'):
|
||||
printer.ip_address = data['ipAddress']
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(printer.to_dict())
|
||||
|
||||
@app.route('/api/printers/<printer_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_printer(printer_id):
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
db.session.delete(printer)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Drucker gelöscht!'})
|
||||
|
||||
@app.route('/api/jobs', methods=['GET'])
|
||||
@login_required
|
||||
def get_jobs():
|
||||
# Admins sehen alle Jobs, normale Benutzer nur ihre eigenen
|
||||
if g.current_user.role == 'admin':
|
||||
jobs = PrintJob.query.all()
|
||||
else:
|
||||
jobs = PrintJob.query.filter_by(user_id=g.current_user.id).all()
|
||||
|
||||
return jsonify([job.to_dict() for job in jobs])
|
||||
|
||||
@app.route('/api/jobs', methods=['POST'])
|
||||
@login_required
|
||||
def create_job():
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('printerId') or not data.get('durationInMinutes'):
|
||||
return jsonify({'message': 'Drucker-ID und Dauer sind erforderlich!'}), 400
|
||||
|
||||
printer = Printer.query.get_or_404(data['printerId'])
|
||||
|
||||
if printer.status != 0: # 0 = available
|
||||
return jsonify({'message': 'Drucker ist nicht verfügbar!'}), 400
|
||||
|
||||
duration = int(data['durationInMinutes'])
|
||||
|
||||
job = PrintJob(
|
||||
printer_id=printer.id,
|
||||
user_id=g.current_user.id,
|
||||
duration_in_minutes=duration,
|
||||
comments=data.get('comments', '')
|
||||
)
|
||||
|
||||
# Drucker als belegt markieren
|
||||
printer.status = 1 # 1 = busy
|
||||
|
||||
db.session.add(job)
|
||||
db.session.commit()
|
||||
|
||||
# Steckdose einschalten, falls IP-Adresse hinterlegt ist
|
||||
if printer.ip_address:
|
||||
try:
|
||||
asyncio.run(tapo_control.turn_on(printer.ip_address))
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler beim Einschalten der Steckdose: {e}")
|
||||
|
||||
return jsonify(job.to_dict()), 201
|
||||
|
||||
@app.route('/api/jobs/<job_id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_job(job_id):
|
||||
# Admins können alle Jobs sehen, Benutzer nur ihre eigenen
|
||||
if g.current_user.role == 'admin':
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
else:
|
||||
job = PrintJob.query.filter_by(id=job_id, user_id=g.current_user.id).first_or_404()
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@app.route('/api/jobs/<job_id>/abort', methods=['POST'])
|
||||
@login_required
|
||||
def abort_job(job_id):
|
||||
# Admins können alle Jobs abbrechen, Benutzer nur ihre eigenen
|
||||
if g.current_user.role == 'admin':
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
else:
|
||||
job = PrintJob.query.filter_by(id=job_id, user_id=g.current_user.id).first_or_404()
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
job.aborted = True
|
||||
job.abort_reason = data.get('reason', '')
|
||||
|
||||
# Drucker wieder verfügbar machen
|
||||
printer = job.printer
|
||||
printer.status = 0 # 0 = available
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Steckdose ausschalten, falls IP-Adresse hinterlegt ist
|
||||
if printer.ip_address:
|
||||
try:
|
||||
asyncio.run(tapo_control.turn_off(printer.ip_address))
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler beim Ausschalten der Steckdose: {e}")
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@app.route('/api/jobs/<job_id>/finish', methods=['POST'])
|
||||
@login_required
|
||||
def finish_job(job_id):
|
||||
# Admins können alle Jobs beenden, Benutzer nur ihre eigenen
|
||||
if g.current_user.role == 'admin':
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
else:
|
||||
job = PrintJob.query.filter_by(id=job_id, user_id=g.current_user.id).first_or_404()
|
||||
|
||||
# Aktuelle Zeit als Ende setzen
|
||||
now = datetime.datetime.utcnow()
|
||||
actual_duration = int((now - job.start_at).total_seconds() / 60)
|
||||
job.duration_in_minutes = actual_duration
|
||||
|
||||
# Drucker wieder verfügbar machen
|
||||
printer = job.printer
|
||||
printer.status = 0 # 0 = available
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Steckdose ausschalten, falls IP-Adresse hinterlegt ist
|
||||
if printer.ip_address:
|
||||
try:
|
||||
asyncio.run(tapo_control.turn_off(printer.ip_address))
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler beim Ausschalten der Steckdose: {e}")
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@app.route('/api/jobs/<job_id>/extend', methods=['POST'])
|
||||
@login_required
|
||||
def extend_job(job_id):
|
||||
# Admins können alle Jobs verlängern, Benutzer nur ihre eigenen
|
||||
if g.current_user.role == 'admin':
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
else:
|
||||
job = PrintJob.query.filter_by(id=job_id, user_id=g.current_user.id).first_or_404()
|
||||
|
||||
data = request.get_json()
|
||||
minutes = int(data.get('minutes', 0))
|
||||
hours = int(data.get('hours', 0))
|
||||
|
||||
additional_minutes = minutes + (hours * 60)
|
||||
if additional_minutes <= 0:
|
||||
return jsonify({'message': 'Ungültige Verlängerungszeit!'}), 400
|
||||
|
||||
job.duration_in_minutes += additional_minutes
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@app.route('/api/jobs/<job_id>/comments', methods=['PUT'])
|
||||
@login_required
|
||||
def update_job_comments(job_id):
|
||||
# Admins können alle Jobs bearbeiten, Benutzer nur ihre eigenen
|
||||
if g.current_user.role == 'admin':
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
else:
|
||||
job = PrintJob.query.filter_by(id=job_id, user_id=g.current_user.id).first_or_404()
|
||||
|
||||
data = request.get_json()
|
||||
job.comments = data.get('comments', '')
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@app.route('/api/job/<job_id>/remaining-time', methods=['GET'])
|
||||
def job_remaining_time(job_id):
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
remaining = job.remaining_time()
|
||||
return jsonify({'remaining_minutes': remaining})
|
||||
|
||||
@app.route('/api/users', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def get_users():
|
||||
users = User.query.all()
|
||||
return jsonify([user.to_dict() for user in users])
|
||||
|
||||
@app.route('/api/users/<user_id>', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def get_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
return jsonify(user.to_dict())
|
||||
|
||||
@app.route('/api/users/<user_id>', methods=['PUT'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def update_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
data = request.get_json()
|
||||
|
||||
if data.get('username'):
|
||||
user.username = data['username']
|
||||
if data.get('displayName'):
|
||||
user.display_name = data['displayName']
|
||||
if data.get('email'):
|
||||
user.email = data['email']
|
||||
if data.get('role'):
|
||||
user.role = data['role']
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict())
|
||||
|
||||
@app.route('/api/users/<user_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
# Löschen aller Sessions des Benutzers
|
||||
Session.query.filter_by(user_id=user.id).delete()
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Benutzer gelöscht!'})
|
||||
|
||||
@app.route('/api/stats', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def stats():
|
||||
# Drucker-Nutzungsstatistiken
|
||||
total_printers = Printer.query.count()
|
||||
available_printers = Printer.query.filter_by(status=0).count()
|
||||
|
||||
# Job-Statistiken
|
||||
total_jobs = PrintJob.query.count()
|
||||
active_jobs = PrintJob.query.filter(
|
||||
PrintJob.aborted == False,
|
||||
PrintJob.start_at + timedelta(minutes=PrintJob.duration_in_minutes) > datetime.datetime.utcnow()
|
||||
).count()
|
||||
completed_jobs = PrintJob.query.filter(
|
||||
PrintJob.aborted == False,
|
||||
PrintJob.start_at + timedelta(minutes=PrintJob.duration_in_minutes) <= datetime.datetime.utcnow()
|
||||
).count()
|
||||
|
||||
# Benutzerstatistiken
|
||||
total_users = User.query.count()
|
||||
|
||||
# Durchschnittliche Druckdauer
|
||||
avg_duration_result = db.session.query(db.func.avg(PrintJob.duration_in_minutes)).first()
|
||||
avg_duration = int(avg_duration_result[0]) if avg_duration_result[0] else 0
|
||||
|
||||
return jsonify({
|
||||
'printers': {
|
||||
'total': total_printers,
|
||||
'available': available_printers,
|
||||
'utilization_rate': (total_printers - available_printers) / total_printers if total_printers > 0 else 0
|
||||
},
|
||||
'jobs': {
|
||||
'total': total_jobs,
|
||||
'active': active_jobs,
|
||||
'completed': completed_jobs,
|
||||
'avg_duration': avg_duration
|
||||
},
|
||||
'users': {
|
||||
'total': total_users
|
||||
}
|
||||
})
|
||||
|
||||
# Tägliche Überprüfung der Jobs und automatische Abschaltung der Steckdosen
|
||||
@app.cli.command("check-jobs")
|
||||
def check_jobs():
|
||||
"""Überprüft abgelaufene Jobs und schaltet Steckdosen aus."""
|
||||
now = datetime.datetime.utcnow()
|
||||
|
||||
# Abgelaufene Jobs finden
|
||||
expired_jobs = PrintJob.query.filter(
|
||||
PrintJob.aborted == False,
|
||||
PrintJob.start_at + timedelta(minutes=PrintJob.duration_in_minutes) <= now
|
||||
).all()
|
||||
|
||||
for job in expired_jobs:
|
||||
printer = job.printer
|
||||
|
||||
# Drucker-Status auf verfügbar setzen
|
||||
if printer.status == 1: # busy
|
||||
printer.status = 0 # available
|
||||
app.logger.info(f"Job {job.id} abgelaufen. Drucker {printer.id} auf verfügbar gesetzt.")
|
||||
|
||||
# Steckdose ausschalten, falls IP-Adresse hinterlegt ist
|
||||
if printer.ip_address:
|
||||
try:
|
||||
asyncio.run(tapo_control.turn_off(printer.ip_address))
|
||||
app.logger.info(f"Steckdose {printer.ip_address} für abgelaufenen Job {job.id} ausgeschaltet.")
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler beim Ausschalten der Steckdose {printer.ip_address}: {e}")
|
||||
|
||||
db.session.commit()
|
||||
app.logger.info(f"{len(expired_jobs)} abgelaufene Jobs überprüft und Steckdosen aktualisiert.")
|
||||
|
||||
@app.route('/api/test', methods=['GET'])
|
||||
def test():
|
||||
return jsonify({'message': 'MYP Backend API funktioniert!'})
|
||||
|
||||
# Error Handler
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return jsonify({'message': 'Nicht gefunden!'}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(error):
|
||||
app.logger.error(f'Serverfehler: {error}')
|
||||
return jsonify({'message': 'Interner Serverfehler!'}), 500
|
||||
|
||||
# Server starten
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0')
|
8
backend/crontab-example
Normal file
8
backend/crontab-example
Normal file
@ -0,0 +1,8 @@
|
||||
# MYP Backend Cron-Jobs
|
||||
# Installiere mit: crontab crontab-example
|
||||
|
||||
# Prüfe alle 5 Minuten auf abgelaufene Reservierungen und schalte Steckdosen aus
|
||||
*/5 * * * * cd /pfad/zum/projektarbeit-myp/backend && /pfad/zur/venv/bin/flask check-jobs >> /pfad/zum/projektarbeit-myp/backend/logs/cron.log 2>&1
|
||||
|
||||
# Tägliche Sicherung der Datenbank um 3:00 Uhr
|
||||
0 3 * * * cd /pfad/zum/projektarbeit-myp/backend && cp instance/myp.db instance/backups/myp-$(date +\%Y\%m\%d).db
|
15
backend/docker-compose.yml
Normal file
15
backend/docker-compose.yml
Normal file
@ -0,0 +1,15 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build: .
|
||||
container_name: myp-backend
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- SECRET_KEY=change_me_in_production
|
||||
- DATABASE_URL=sqlite:///myp.db
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./instance:/app/instance
|
||||
restart: unless-stopped
|
73
backend/install.sh
Executable file
73
backend/install.sh
Executable file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Installation Script für MYP Backend
|
||||
|
||||
echo "=== MYP Backend Installation ==="
|
||||
echo ""
|
||||
|
||||
# Prüfe Python-Version
|
||||
python_version=$(python3 --version 2>&1 | awk '{print $2}')
|
||||
echo "Python-Version: $python_version"
|
||||
|
||||
# Prüfe, ob die Python-Version mindestens 3.8 ist
|
||||
required_version="3.8.0"
|
||||
if [[ "$(printf '%s\n' "$required_version" "$python_version" | sort -V | head -n1)" != "$required_version" ]]; then
|
||||
echo "FEHLER: Python $required_version oder höher wird benötigt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Erstelle virtuelle Umgebung
|
||||
echo ""
|
||||
echo "Erstelle virtuelle Python-Umgebung..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Installiere Abhängigkeiten
|
||||
echo ""
|
||||
echo "Installiere Abhängigkeiten..."
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Erstelle .env-Datei
|
||||
echo ""
|
||||
echo "Erstelle .env-Datei..."
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
echo "Die .env-Datei wurde aus der Beispieldatei erstellt."
|
||||
echo "Bitte passe die Konfiguration für GitHub OAuth und Tapo-Steckdosen an."
|
||||
else
|
||||
echo ".env-Datei existiert bereits."
|
||||
fi
|
||||
|
||||
# Erstelle Logs-Ordner
|
||||
echo ""
|
||||
echo "Erstelle logs-Ordner..."
|
||||
mkdir -p logs
|
||||
|
||||
# Erstelle Instance und Backup Ordner
|
||||
echo ""
|
||||
echo "Erstelle instance-Ordner..."
|
||||
mkdir -p instance/backups
|
||||
|
||||
# Initialisiere die Datenbank
|
||||
echo ""
|
||||
echo "Initialisiere die Datenbank..."
|
||||
FLASK_APP=app.py flask db init
|
||||
FLASK_APP=app.py flask db migrate -m "Initiale Datenbank-Erstellung"
|
||||
FLASK_APP=app.py flask db upgrade
|
||||
|
||||
echo ""
|
||||
echo "=== Installation abgeschlossen ==="
|
||||
echo ""
|
||||
echo "Wichtige Schritte vor dem Start:"
|
||||
echo "1. Konfiguriere die .env-Datei mit deinen GitHub OAuth-Credentials"
|
||||
echo "2. Konfiguriere die Tapo-Steckdosen-Zugangsdaten in der .env-Datei"
|
||||
echo "3. Passe die crontab-example an und installiere den Cron-Job (optional)"
|
||||
echo ""
|
||||
echo "Starte den Server mit:"
|
||||
echo "source venv/bin/activate"
|
||||
echo "python app.py"
|
||||
echo ""
|
||||
echo "Oder mit Gunicorn für Produktion:"
|
||||
echo "gunicorn --bind 0.0.0.0:5000 app:app"
|
||||
echo ""
|
12
backend/requirements.txt
Normal file
12
backend/requirements.txt
Normal file
@ -0,0 +1,12 @@
|
||||
flask==2.3.3
|
||||
flask-cors==4.0.0
|
||||
flask-sqlalchemy==3.1.1
|
||||
flask-migrate==4.0.5
|
||||
pyjwt==2.8.0
|
||||
python-dotenv==1.0.0
|
||||
werkzeug==2.3.7
|
||||
gunicorn==21.2.0
|
||||
authlib==1.2.1
|
||||
tapo==0.9.3.1
|
||||
aiohttp==3.8.5
|
||||
requests==2.31.0
|
253
backend/tests.py
Normal file
253
backend/tests.py
Normal file
@ -0,0 +1,253 @@
|
||||
import unittest
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app import app, db, User, Printer, PrintJob
|
||||
|
||||
class MYPBackendTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Temporäre Datenbank für Tests
|
||||
self.db_fd, app.config['DATABASE'] = tempfile.mkstemp()
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE']
|
||||
app.config['TESTING'] = True
|
||||
self.app = app.test_client()
|
||||
|
||||
# Datenbank-Tabellen erstellen und Test-Daten einfügen
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Admin-Benutzer erstellen
|
||||
admin = User(username='admin_test', email='admin@test.com', role='admin')
|
||||
admin.set_password('admin')
|
||||
db.session.add(admin)
|
||||
|
||||
# Normaler Benutzer erstellen
|
||||
user = User(username='user_test', email='user@test.com', role='user')
|
||||
user.set_password('user')
|
||||
db.session.add(user)
|
||||
|
||||
# Drucker erstellen
|
||||
printer1 = Printer(name='Printer 1', location='Room A', type='3D',
|
||||
status='available', description='Test printer 1')
|
||||
printer2 = Printer(name='Printer 2', location='Room B', type='3D',
|
||||
status='busy', description='Test printer 2')
|
||||
db.session.add(printer1)
|
||||
db.session.add(printer2)
|
||||
|
||||
# Job erstellen
|
||||
start_time = datetime.utcnow()
|
||||
end_time = start_time + timedelta(minutes=60)
|
||||
job = PrintJob(title='Test Job', start_time=start_time, end_time=end_time,
|
||||
duration=60, status='active', comments='Test job',
|
||||
user_id=2, printer_id=2)
|
||||
db.session.add(job)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def tearDown(self):
|
||||
# Aufräumen nach dem Test
|
||||
os.close(self.db_fd)
|
||||
os.unlink(app.config['DATABASE'])
|
||||
|
||||
def get_token(self, username, password):
|
||||
response = self.app.post('/api/auth/login',
|
||||
data=json.dumps({'username': username, 'password': password}),
|
||||
content_type='application/json')
|
||||
data = json.loads(response.data)
|
||||
return data.get('token')
|
||||
|
||||
def test_login(self):
|
||||
# Test: Erfolgreicher Login
|
||||
response = self.app.post('/api/auth/login',
|
||||
data=json.dumps({'username': 'admin_test', 'password': 'admin'}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('token', data)
|
||||
self.assertIn('user', data)
|
||||
|
||||
# Test: Fehlgeschlagener Login (falsches Passwort)
|
||||
response = self.app.post('/api/auth/login',
|
||||
data=json.dumps({'username': 'admin_test', 'password': 'wrong'}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_register(self):
|
||||
# Test: Erfolgreiche Registrierung
|
||||
response = self.app.post('/api/auth/register',
|
||||
data=json.dumps({
|
||||
'username': 'new_user',
|
||||
'email': 'new@test.com',
|
||||
'password': 'password'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# Test: Doppelte Registrierung
|
||||
response = self.app.post('/api/auth/register',
|
||||
data=json.dumps({
|
||||
'username': 'new_user',
|
||||
'email': 'another@test.com',
|
||||
'password': 'password'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_get_printers(self):
|
||||
# Test: Drucker abrufen
|
||||
response = self.app.get('/api/printers')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(len(data), 2)
|
||||
|
||||
def test_get_single_printer(self):
|
||||
# Test: Einzelnen Drucker abrufen
|
||||
response = self.app.get('/api/printers/1')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['name'], 'Printer 1')
|
||||
|
||||
def test_create_printer(self):
|
||||
# Als Admin einen Drucker erstellen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.post('/api/printers',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'name': 'New Printer',
|
||||
'location': 'Room C',
|
||||
'type': '3D',
|
||||
'description': 'New test printer'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['name'], 'New Printer')
|
||||
|
||||
def test_update_printer(self):
|
||||
# Als Admin einen Drucker aktualisieren
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.put('/api/printers/1',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'name': 'Updated Printer',
|
||||
'location': 'Room D'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['name'], 'Updated Printer')
|
||||
self.assertEqual(data['location'], 'Room D')
|
||||
|
||||
def test_delete_printer(self):
|
||||
# Als Admin einen Drucker löschen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.delete('/api/printers/1',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Überprüfen, ob der Drucker wirklich gelöscht wurde
|
||||
response = self.app.get('/api/printers/1')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_get_jobs_as_admin(self):
|
||||
# Als Admin alle Jobs abrufen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.get('/api/jobs',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(len(data), 1)
|
||||
|
||||
def test_get_jobs_as_user(self):
|
||||
# Als normaler Benutzer nur eigene Jobs abrufen
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.get('/api/jobs',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(len(data), 1) # Der Benutzer hat einen Job
|
||||
|
||||
def test_create_job(self):
|
||||
# Als Benutzer einen Job erstellen
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.post('/api/jobs',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'title': 'New Job',
|
||||
'printer_id': 1,
|
||||
'duration': 30,
|
||||
'comments': 'Test job creation'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['title'], 'New Job')
|
||||
self.assertEqual(data['duration'], 30)
|
||||
|
||||
def test_update_job(self):
|
||||
# Als Benutzer den eigenen Job aktualisieren
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.put('/api/jobs/1',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'comments': 'Updated comments',
|
||||
'duration': 15 # Verlängerung
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['comments'], 'Updated comments')
|
||||
self.assertEqual(data['duration'], 75) # 60 + 15
|
||||
|
||||
def test_complete_job(self):
|
||||
# Als Benutzer einen Job als abgeschlossen markieren
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.put('/api/jobs/1',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'status': 'completed'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['status'], 'completed')
|
||||
|
||||
# Überprüfen, ob der Drucker wieder verfügbar ist
|
||||
response = self.app.get('/api/printers/2')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['status'], 'available')
|
||||
|
||||
def test_get_remaining_time(self):
|
||||
# Test: Verbleibende Zeit für einen aktiven Job abrufen
|
||||
response = self.app.get('/api/job/1/remaining-time')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('remaining_minutes', data)
|
||||
# Der genaue Wert kann nicht überprüft werden, da er von der Zeit abhängt
|
||||
|
||||
def test_stats(self):
|
||||
# Als Admin Statistiken abrufen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.get('/api/stats',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('printers', data)
|
||||
self.assertIn('jobs', data)
|
||||
self.assertIn('users', data)
|
||||
self.assertEqual(data['printers']['total'], 2)
|
||||
self.assertEqual(data['jobs']['total'], 1)
|
||||
self.assertEqual(data['users']['total'], 2)
|
||||
|
||||
def test_test_endpoint(self):
|
||||
# Test: API-Test-Endpunkt
|
||||
response = self.app.get('/api/test')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['message'], 'MYP Backend API funktioniert!')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user