Compare commits
119 Commits
torben-fro
...
main
Author | SHA1 | Date | |
---|---|---|---|
32ee5f065c | |||
dd31f9fa4e | |||
|
069ceb47b2 | ||
60f8a87028 | |||
5957e8104c | |||
1348c5479e | |||
|
5807303d90 | ||
94532743ad | |||
ef04d7fd0f | |||
3d576f0642 | |||
290d5b0ff2 | |||
a7760a12ce | |||
a9a1bf52db | |||
|
2602e42d0b | ||
273b78f12a | |||
b53ffaec44 | |||
cef6abec32 | |||
1f6feafecc | |||
de163719aa | |||
4e5fd491ae | |||
aa8f3b96f3 | |||
b65478ed57 | |||
76a4299eb0 | |||
78d19daf14 | |||
|
ffefd51c0d | ||
f64ca592c3 | |||
0109eebab6 | |||
4a994c3bf8 | |||
7eabb59b35 | |||
|
f1a2ca75b1 | ||
a757c24f12 | |||
|
845e6dcc24 | ||
ee15efc898 | |||
b0eef79b1d | |||
|
659af9abc0 | ||
|
ee4e4b0a56 | ||
04ff95469b | |||
|
6404f011cc | ||
b35a66cd8a | |||
|
8a4542c6bf | ||
59b9189686 | |||
|
9a0cda9cad | ||
|
77b89186d3 | ||
ef6b46dc05 | |||
05bd3f3f22 | |||
e143f4ab16 | |||
f1541478ad | |||
fc62086a50 | |||
951473d1ec | |||
4db5512b57 | |||
347cc931ed | |||
a082a81c87 | |||
7f0991e517 | |||
621833766d | |||
27e1b2d82c | |||
4e0fa33dee | |||
|
37f2519140 | ||
f26759ec24 | |||
|
3ffde06e6e | ||
3e08a09d87 | |||
8c3c80fb5c | |||
fa3a2209ad | |||
2ab4c4c3e2 | |||
8366a9295e | |||
8db9a93507 | |||
d496dec773 | |||
37ab57c455 | |||
aad1be90ee | |||
|
4fda1139d1 | ||
7a34f7581c | |||
8ba0f9e55a | |||
38d1a90dc2 | |||
84a3f45d2f | |||
5c9de8becc | |||
|
9502a9b3af | ||
|
2ab091b90c | ||
|
9ffa70aad1 | ||
|
b0d8d4f915 | ||
|
52a336a788 | ||
|
63b04c4dea | ||
be0067ab06 | |||
|
f58c85c8f0 | ||
|
84ddfefaea | ||
4a2782734a | |||
|
190794b2c1 | ||
|
ad5bd4367e | ||
|
097934ac18 | ||
|
a7b8d470e4 | ||
|
3f2be5b17d | ||
7bf9d7e5ae | |||
a651257b3b | |||
|
3972860be8 | ||
|
6cdc437d3e | ||
|
7c1d0069c6 | ||
|
575325e838 | ||
|
faf0736dbd | ||
|
47143d29a5 | ||
|
8222d89b2b | ||
|
ea4b903d63 | ||
f3cd2ba730 | |||
b5dcc6999d | |||
038c261eb7 | |||
|
2adafb149a | ||
|
e31c4036d7 | ||
|
29730fa880 | ||
c3fa6455d0 | |||
7061d13b12 | |||
|
331a235f05 | ||
|
70aeb17cdb | ||
|
73ead5939c | ||
|
f480ed00bd | ||
|
61b5a4a67c | ||
55936c81f0 | |||
68a1910bdc | |||
b7fe9a036a | |||
fc911317f4 | |||
73dd2e5a84 | |||
fb2d584874 | |||
dfd63d7c9d |
53
CLAUDE.md
Normal file
53
CLAUDE.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# MYP Project Development Guidelines
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
- **Frontend**:
|
||||||
|
- Located in `packages/reservation-platform`
|
||||||
|
- Runs on a Raspberry Pi connected to company network
|
||||||
|
- Has internet access on one interface
|
||||||
|
- Connected via LAN to an offline network
|
||||||
|
- Serves as the user interface
|
||||||
|
- Developed by another apprentice as part of IHK project work
|
||||||
|
|
||||||
|
- **Backend**:
|
||||||
|
- Located in `backend` directory
|
||||||
|
- Flask application running on a separate Raspberry Pi
|
||||||
|
- Connected only to the offline network
|
||||||
|
- Communicates with WiFi smart plugs
|
||||||
|
- Part of my IHK project work for digital networking qualification
|
||||||
|
|
||||||
|
- **Printers/Smart Plugs**:
|
||||||
|
- Printers can only be controlled (on/off) via WiFi smart plugs
|
||||||
|
- No other control mechanisms available
|
||||||
|
- Smart plugs and printers are equivalent in the system context
|
||||||
|
|
||||||
|
## Build/Run Commands
|
||||||
|
- Backend: `cd backend && source venv/bin/activate && python app.py`
|
||||||
|
- Frontend: `cd packages/reservation-platform && pnpm dev`
|
||||||
|
- Run tests: `cd backend && python -m unittest development/tests/tests.py`
|
||||||
|
- Run single test: `cd backend && python -m unittest development.tests.tests.MYPBackendTestCase.test_name`
|
||||||
|
- Check jobs manually: `cd backend && source venv/bin/activate && flask check-jobs`
|
||||||
|
- Lint frontend: `cd packages/reservation-platform && pnpm lint`
|
||||||
|
- Format frontend: `cd packages/reservation-platform && npx @biomejs/biome format --write ./src`
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
- **Python Backend**:
|
||||||
|
- Use PEP 8 conventions, 4-space indentation
|
||||||
|
- Line width: 100 characters max
|
||||||
|
- Add docstrings to functions and classes
|
||||||
|
- Error handling: Use try/except with specific exceptions
|
||||||
|
- Naming: snake_case for functions/variables, PascalCase for classes
|
||||||
|
|
||||||
|
- **Frontend (Next.js/TypeScript)**:
|
||||||
|
- Use Biome for formatting and linting (line width: 120 chars)
|
||||||
|
- Organize imports automatically with Biome
|
||||||
|
- Use TypeScript types for all code
|
||||||
|
- Use React hooks for state management
|
||||||
|
- Naming: camelCase for functions/variables, PascalCase for components
|
||||||
|
|
||||||
|
## Work Guidelines
|
||||||
|
- All changes must be committed to git
|
||||||
|
- Work efficiently and cost-effectively
|
||||||
|
- Don't repeatedly try the same solution if it doesn't work
|
||||||
|
- Create and check notes when encountering issues
|
||||||
|
- Clearly communicate if something is not possible so I can handle it manually
|
3
CREDENTIALS
Normal file
3
CREDENTIALS
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
TAPO ADMIN: vT6Vsd^p
|
||||||
|
Admin-PW: 744563017196
|
||||||
|
Tapo: 744563017196A
|
11
Dokumentation.md
Executable file
11
Dokumentation.md
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
# Dokumentation
|
||||||
|
|
||||||
|
Komplikationen:
|
||||||
|
- Netzwerkanbindung
|
||||||
|
- Ermitteln der Schnittstellen der Drucker
|
||||||
|
- Auswahl der Anbindung, Entwickeln eines Netzwerkkonzeptes
|
||||||
|
- Beschaffung der Hardware (beschränkte Auswahlmöglichkeiten)
|
||||||
|
- Welches Betriebssystem? OpenSuse, NixOS, Debian
|
||||||
|
- Frontend verstehen lernen
|
||||||
|
- Netzwerk einrichten, Frontend anbinden
|
||||||
|
|
0
LICENSE.md
Normal file → Executable file
0
LICENSE.md
Normal file → Executable file
5
README.md
Normal file → Executable file
5
README.md
Normal file → Executable file
@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
# 📦 MYP
|
# 📦 MYP
|
||||||
|
|
||||||
> Frontend: https://git.i.mercedes-benz.com/TBA-Berlin-FI/MYP/tree/main/packages/reservation-platform
|
> Frontend: https://git.i.mercedes-benz.com/TBA-Berlin-FI/MYP/tree/main/packages/reservation-platform
|
||||||
@ -44,3 +45,7 @@ MYP *(Manage your Printer)* ist eine Plattform zur Reservierung von 3D-Druckern,
|
|||||||
## Fremdschlüsselbeziehungen
|
## Fremdschlüsselbeziehungen
|
||||||
- `User` ist verknüpft mit `PrintJob`, `Account` und `Session` über Benutzer-ID.
|
- `User` ist verknüpft mit `PrintJob`, `Account` und `Session` über Benutzer-ID.
|
||||||
- `Printer` ist verknüpft mit `PrintJob` über die Drucker-ID.
|
- `Printer` ist verknüpft mit `PrintJob` über die Drucker-ID.
|
||||||
|
=======
|
||||||
|
# Projektarbeit-MYP
|
||||||
|
|
||||||
|
>>>>>>> dfd63d7c9ddf4b3a654f06dff38bebdbec7395d7
|
||||||
|
5
backend/.env
Normal file
5
backend/.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
||||||
|
DATABASE_PATH=instance/myp.db
|
||||||
|
TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
||||||
|
TAPO_PASSWORD=744563017196A
|
||||||
|
PRINTERS={"Printer 1": {"ip": "192.168.0.100"}, "Printer 2": {"ip": "192.168.0.101"}, "Printer 3": {"ip": "192.168.0.102"}, "Printer 4": {"ip": "192.168.0.103"}, "Printer 5": {"ip": "192.168.0.104"}, "Printer 6": {"ip": "192.168.0.106"}}
|
48
backend/.gitignore
vendored
Executable file
48
backend/.gitignore
vendored
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# SQLite Datenbank-Dateien
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# Virtuelle Umgebungen
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Betriebssystem
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
52
backend/Dockerfile
Executable file
52
backend/Dockerfile
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
FROM python:slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies (curl, sqlite3 for database, wget for healthcheck)
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
sqlite3 \
|
||||||
|
wget \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
RUN mkdir -p logs instance
|
||||||
|
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Add health check endpoint
|
||||||
|
RUN echo 'from flask import Blueprint\n\
|
||||||
|
health_bp = Blueprint("health", __name__)\n\
|
||||||
|
\n\
|
||||||
|
@health_bp.route("/health")\n\
|
||||||
|
def health_check():\n\
|
||||||
|
return {"status": "healthy"}, 200\n'\
|
||||||
|
> /app/health.py
|
||||||
|
|
||||||
|
# Add the health blueprint to app.py if it doesn't exist
|
||||||
|
RUN grep -q "health_bp" app.py || sed -i '/from flask import/a from health import health_bp' app.py
|
||||||
|
RUN grep -q "app.register_blueprint(health_bp)" app.py || sed -i '/app = Flask/a app.register_blueprint(health_bp)' app.py
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Add startup script to initialize database if needed
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
if [ ! -f "instance/myp.db" ] || [ ! -s "instance/myp.db" ]; then\n\
|
||||||
|
echo "Initializing database..."\n\
|
||||||
|
python -c "from app import init_db; init_db()"\n\
|
||||||
|
fi\n\
|
||||||
|
\n\
|
||||||
|
echo "Starting gunicorn server..."\n\
|
||||||
|
gunicorn --bind 0.0.0.0:5000 app:app\n'\
|
||||||
|
> /app/start.sh && chmod +x /app/start.sh
|
||||||
|
|
||||||
|
CMD ["/app/start.sh"]
|
1719
backend/app.py
Executable file
1719
backend/app.py
Executable file
File diff suppressed because it is too large
Load Diff
8
backend/development/crontab-example
Normal file
8
backend/development/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
|
84
backend/development/initialize_myp_database.sh
Normal file
84
backend/development/initialize_myp_database.sh
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# MYP Datenbank Initialisierungs-Skript
|
||||||
|
# Dieses Skript erstellt die erforderlichen Datenbanktabellen für das MYP Backend
|
||||||
|
|
||||||
|
echo "=== MYP Datenbank Initialisierung ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe, ob sqlite3 installiert ist
|
||||||
|
if ! command -v sqlite3 &> /dev/null; then
|
||||||
|
echo "FEHLER: sqlite3 ist nicht installiert."
|
||||||
|
echo "Bitte installiere sqlite3 mit deinem Paketmanager, z.B. 'apt install sqlite3'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle Instance-Ordner, falls nicht vorhanden
|
||||||
|
echo "Erstelle instance-Ordner, falls nicht vorhanden..."
|
||||||
|
mkdir -p instance/backups
|
||||||
|
|
||||||
|
# Prüfen, ob die Datenbank bereits existiert
|
||||||
|
if [ -f "instance/myp.db" ]; then
|
||||||
|
echo "Datenbank existiert bereits."
|
||||||
|
echo "Erstelle Backup in instance/backups..."
|
||||||
|
cp instance/myp.db "instance/backups/myp_$(date '+%Y%m%d_%H%M%S').db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle die Datenbank und ihre Tabellen
|
||||||
|
echo "Erstelle neue Datenbank..."
|
||||||
|
sqlite3 instance/myp.db <<EOF
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
display_name TEXT,
|
||||||
|
email TEXT UNIQUE,
|
||||||
|
role TEXT DEFAULT 'user'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS session (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS socket (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
status INTEGER DEFAULT 0,
|
||||||
|
ip_address TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS job (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
socket_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
start_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
duration_in_minutes INTEGER NOT NULL,
|
||||||
|
comments TEXT,
|
||||||
|
aborted INTEGER DEFAULT 0,
|
||||||
|
abort_reason TEXT,
|
||||||
|
FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Setze Berechtigungen für die Datenbankdatei
|
||||||
|
chmod 644 instance/myp.db
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Datenbank-Initialisierung abgeschlossen ==="
|
||||||
|
echo ""
|
||||||
|
echo "Du kannst jetzt einen Admin-Benutzer über die Web-Oberfläche registrieren."
|
||||||
|
echo "Der erste registrierte Benutzer wird automatisch zum Admin."
|
||||||
|
echo ""
|
||||||
|
echo "Starte den Server mit:"
|
||||||
|
echo "python app.py"
|
||||||
|
echo ""
|
||||||
|
echo "Alternativ kannst du einen Admin-Benutzer über die API erstellen mit:"
|
||||||
|
echo "curl -X POST http://localhost:5000/api/create-initial-admin -H \"Content-Type: application/json\" -d '{\"username\":\"admin\",\"password\":\"password\",\"displayName\":\"Administrator\"}'"
|
||||||
|
echo ""
|
95
backend/development/tests/api-test.drucker.py
Normal file
95
backend/development/tests/api-test.drucker.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Basis-URL inkl. Token
|
||||||
|
url = "http://192.168.0.102:80/app?token=9DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
||||||
|
|
||||||
|
# HTTP-Header wie in der Originalanfrage
|
||||||
|
headers = {
|
||||||
|
"Referer": "http://192.168.0.102:80",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"requestByApp": "true",
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
"Host": "192.168.0.102",
|
||||||
|
"Connection": "Keep-Alive",
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
"User-Agent": "okhttp/3.14.9"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Liste der Payloads (als Python-Dictionaries)
|
||||||
|
payloads = [
|
||||||
|
{
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": (
|
||||||
|
"ZC4CHp6bbfBO1rtmuH6I+TStBIiFRfQpayYPwet5NBmL35dib5xXHeEeLM7c0OSQSyxO6fnbXrC1\n"
|
||||||
|
"gXdfowwwq4Fum9ispgt8yT7cgbDcqnoVrhxEtHIDfuwLh8YAGmDSfTMo/JlsGspWPYMKd1EWXtb5\n"
|
||||||
|
"gP9FA9LHnV2kxKsNSPQ=\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": (
|
||||||
|
"k111EbfCcfVzAouNbu1vyos9Ltsg+a97n4xUUQMviQVJfhqxvKOhv1SrvEk2LvpD0LwNVUNPZdwU\n"
|
||||||
|
"6pH5E/NOwdc1WzTPeqHiY760GpUuqn0tToHEHEyO2HaSKdrAYnw2gN410bvHb0pM3gYWS43eOA==\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": (
|
||||||
|
"7/uYVDwyNfFhg9y7rHyp+4AGKBYQPyaBN6cFMl9j4ER/JpJTcGBdaUteSmx8P8Fkz+b2kkNLjYa2\n"
|
||||||
|
"wQr2gA3m6vEq9jpnAF2V3fv9c4Yg9gja9MlTIZqM6EdMi7YbfbhLme34Bh8kMcohDR3u1F4DwFDz\n"
|
||||||
|
"hNZPckf/CegbY9KGFeGwT4rWyX3BTk9+FE7ldtJn\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": (
|
||||||
|
"EjWZb+YYS9tihgLdX4x+Wwx7q+e5X/ZHicr4jOnYmpFToDANzpm5ZpzD49BITcTCdQMOHlJBis85\n"
|
||||||
|
"9GX6Hv8j66OITyH0XmfG9dQo2tgIykyagCZIofr/BpAWYX4aRaOkU4z14mVa2XpDtHJQjc+pXYkh\n"
|
||||||
|
"JuWvLE+h01U5RoyPtvE=\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": (
|
||||||
|
"OwyTsm5HdB/ReJMhVRrkjnV0NLTanw6iXOxVrDDexT456edWuwKiBOsZUyBHmUyJKgiPQzOXqyWWi220bX8IjLX4q8YNgPwRlj+7nRbfzpC/I57wBZBTWIt626pSdIH0vpiuPq84KMfPD5BB2p78/LjsqlzyeLGYzkSsGRBMT8TnLMDFzZE864nfDUZ9muH2kk8NRMN9l6xoCXBJqGA9q8XxIWRTpsl0kTx52kUszY69hYlfFSrrCDIls1ykul14/T1NtOVF8KOgiwaSGOZf7L4QlbhYvRj9kkVVkrxhlwt8jtMqfJKEqq+CIPh3Mp4440WYMLRo6VNIEJ3pWjplkJmc+htnYC4FwVgT7mHZ8eeGGKBvsJz+78gTaHnGBnwZ26I8UdFparyp6QXpOhK9zFmGVh0yapiTHo6jOOI+4Q3Ru+aPnidX/ZASPmR7CZO70CUpvv9zIKJnrAaoTMmH7A6+kcmCRLgLFaTaM+4DFmiz6JGP+4W7MmVPJxxvn0IFlo1P/xwNDuL3T6GLUIEVNk89JG5roBm7AdchUZJO38dGZ0eFiiTK/NhKPvjj+fk9A4FGh7EDshXZhL2u50cdLcdUtcP/CAMDjgWlMm4Kk3vxMQO+UGE+jsB7NkaulmTW1jcl+PSnAE5P71oqVVQ0ng==\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": (
|
||||||
|
"7/uYVDwyNfFhg9y7rHyp+4AGKBYQPyaBN6cFMl9j4ER/JpJTcGBdaUteSmx8P8FkURmv/LWV1FpO\n"
|
||||||
|
"M3RWvsiC5UAsei2G+vwTVuQpOPjKKAx+qwftr9Qs2mSkPNjNLpWHK68EZkIw+h04TQkt0Q99Dirg\n"
|
||||||
|
"0BcrPgHTVKjiK8mdZ6w6gcld/h/FOKYMqJrP0Z+2\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": (
|
||||||
|
"ZE/+XlUmTA9D3DFfp4x3xhS3vdsQ+60tz4TOodtZDby/4DPoqk9EBvJZ1JtUCr5c0AHuv/sfwcvN\n"
|
||||||
|
"Vx1zJP9RkltrAKVTWoaESAeewLozpXt/x0s/jkYC1rh7eTrxm+nYTZ5LJgNtcQq8yJxhEPez1w==\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sende die Payloads sequenziell per POST-Anfrage
|
||||||
|
for idx, payload in enumerate(payloads, start=1):
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
print(f"Anfrage {idx}:")
|
||||||
|
print("Status Code:", response.status_code)
|
||||||
|
print("Response Text:", response.text)
|
||||||
|
print("-" * 60)
|
BIN
backend/development/tests/capture.pcap
Normal file
BIN
backend/development/tests/capture.pcap
Normal file
Binary file not shown.
128
backend/development/tests/handshake.py
Normal file
128
backend/development/tests/handshake.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Constants from the Wireshark capture
|
||||||
|
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||||
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCMl89OZsjqE8yZ9TQhUb9h539WTX3U8Y5YCNdp
|
||||||
|
OhuXvLFYcAT5mvC074VFROmD0xhvw5hrwESOisqpPPU9r78JpLuYUKd+/aidvykqBT8OW5rDLb6d
|
||||||
|
O9FO6Gc+bV8L8ttHVlBFoX69EqiRhcreGPG6FQz4JqGJF4T1nFi0EvALXwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----"""
|
||||||
|
|
||||||
|
# Vorbereitete verschlüsselte Befehle (aus Wireshark extrahiert)
|
||||||
|
COMMAND_ON = """ps0Puxc37EK4PhfcevceL3lyyDrjwLT1+443DDXNbcNRsltlgCQ6+oXgsrE2Pl5OhV73ZI/oM5Nj
|
||||||
|
37cWEaHpXPiHdr1W0cD3aJ5qJ55TfTRkHP9xcMNQJHCn6aWPEHpR7xvvXW9WbJWfShnE2Xdvmw==
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMMAND_OFF = """FlO5i3DRcrUmu2ZwIIv8b68EisGu8VCuqfGOydaR+xCA0n3f2W/EcqVj8MurRBFXYTrZ/uwa1W26
|
||||||
|
ftCfvhdXNebBRwHr9Rj3id4bVfltJ8eT5/R3xY8kputklW2mrw9UfdISzAJqOPp9KZcU4K9p8g==
|
||||||
|
"""
|
||||||
|
|
||||||
|
class TapoP115Controller:
|
||||||
|
def __init__(self, device_ip):
|
||||||
|
self.device_ip = device_ip
|
||||||
|
self.session_id = None
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def perform_handshake(self):
|
||||||
|
"""Führt den ersten Handshake durch und speichert die Session-ID"""
|
||||||
|
handshake_data = {
|
||||||
|
"method": "handshake",
|
||||||
|
"params": {
|
||||||
|
"key": PUBLIC_KEY
|
||||||
|
},
|
||||||
|
"requestTimeMils": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Referer": f"http://{self.device_ip}:80",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"requestByApp": "true",
|
||||||
|
"Content-Type": "application/json; charset=UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"http://{self.device_ip}/app",
|
||||||
|
json=handshake_data,
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data["error_code"] == 0:
|
||||||
|
# Session-ID aus dem Cookie extrahieren
|
||||||
|
self.session_id = response.cookies.get("TP_SESSIONID")
|
||||||
|
print(f"Handshake erfolgreich, Session-ID: {self.session_id}")
|
||||||
|
|
||||||
|
# In einem echten Szenario würden wir hier den verschlüsselten Schlüssel entschlüsseln
|
||||||
|
# Da wir keinen privaten Schlüssel haben, speichern wir nur die Antwort
|
||||||
|
encrypted_key = data["result"]["key"]
|
||||||
|
print(f"Verschlüsselter Schlüssel: {encrypted_key}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print("Handshake fehlgeschlagen")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_command(self, encrypted_command):
|
||||||
|
"""Sendet einen vorbereiteten verschlüsselten Befehl"""
|
||||||
|
if not self.session_id:
|
||||||
|
print("Keine Session-ID. Bitte zuerst Handshake durchführen.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Token aus der Wireshark-Aufnahme (könnte sich ändern, oder vom Gerät abhängen)
|
||||||
|
token = "9DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
||||||
|
|
||||||
|
secure_data = {
|
||||||
|
"method": "securePassthrough",
|
||||||
|
"params": {
|
||||||
|
"request": encrypted_command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Referer": f"http://{self.device_ip}:80",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"requestByApp": "true",
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
"Cookie": f"TP_SESSIONID={self.session_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"http://{self.device_ip}/app?token={token}",
|
||||||
|
json=secure_data,
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data["error_code"] == 0:
|
||||||
|
# In einem echten Szenario würden wir die Antwort entschlüsseln
|
||||||
|
encrypted_response = data["result"]["response"]
|
||||||
|
print("Befehl erfolgreich gesendet")
|
||||||
|
return encrypted_response
|
||||||
|
|
||||||
|
print("Fehler beim Senden des Befehls")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Schaltet die Steckdose ein"""
|
||||||
|
return self.send_command(COMMAND_ON)
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Schaltet die Steckdose aus"""
|
||||||
|
return self.send_command(COMMAND_OFF)
|
||||||
|
|
||||||
|
# Verwendungsbeispiel
|
||||||
|
if __name__ == "__main__":
|
||||||
|
controller = TapoP115Controller("192.168.0.102")
|
||||||
|
|
||||||
|
# Handshake durchführen
|
||||||
|
if controller.perform_handshake():
|
||||||
|
# Steckdose einschalten
|
||||||
|
controller.turn_on()
|
||||||
|
|
||||||
|
# Kurze Pause (im echten Code mit time.sleep)
|
||||||
|
print("Steckdose ist eingeschaltet")
|
||||||
|
|
||||||
|
# Steckdose ausschalten
|
||||||
|
controller.turn_off()
|
||||||
|
print("Steckdose ist ausgeschaltet")
|
9
backend/development/tests/tapo.py
Normal file
9
backend/development/tests/tapo.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from PyP100 import PyP100
|
||||||
|
|
||||||
|
p100 = PyP100.P100("192.168.0.102", "till.tomczak@mercedes-benz.com", "Agent045") #Creates a P100 plug object
|
||||||
|
|
||||||
|
p100.handshake() #Creates the cookies required for further methods
|
||||||
|
p100.login() #Sends credentials to the plug and creates AES Key and IV for further methods
|
||||||
|
|
||||||
|
p100.turnOn() #Turns the connected plug on
|
||||||
|
p100.turnOff() #Turns the connected plug off
|
253
backend/development/tests/tests.py
Normal file
253
backend/development/tests/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()
|
25
backend/docker-compose.yml
Executable file
25
backend/docker-compose.yml
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: .
|
||||||
|
container_name: myp-backend
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
- SECRET_KEY=${SECRET_KEY:-7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F}
|
||||||
|
- DATABASE_PATH=${DATABASE_PATH:-instance/myp.db}
|
||||||
|
- TAPO_USERNAME=${TAPO_USERNAME:-till.tomczak@mercedes-benz.com}
|
||||||
|
- TAPO_PASSWORD=${TAPO_PASSWORD:-744563017196A}
|
||||||
|
- "PRINTERS=${PRINTERS:-{\"Printer 1\": {\"ip\": \"192.168.0.100\"}, \"Printer 2\": {\"ip\": \"192.168.0.101\"}, \"Printer 3\": {\"ip\": \"192.168.0.102\"}, \"Printer 4\": {\"ip\": \"192.168.0.103\"}, \"Printer 5\": {\"ip\": \"192.168.0.104\"}, \"Printer 6\": {\"ip\": \"192.168.0.106\"}}}"
|
||||||
|
- FLASK_APP=app.py
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./instance:/app/instance
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:5000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
647
backend/docs/API_DOCS.md
Normal file
647
backend/docs/API_DOCS.md
Normal file
@ -0,0 +1,647 @@
|
|||||||
|
# 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 Cookies/Sessions, die bei der Anmeldung erstellt werden. Die Session wird für 7 Tage gespeichert.
|
||||||
|
|
||||||
|
### Benutzerregistrierung
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /auth/register`
|
||||||
|
|
||||||
|
**Beschreibung:** Registriert einen neuen Benutzer im System.
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "string", // Erforderlich
|
||||||
|
"password": "string", // Erforderlich
|
||||||
|
"displayName": "string", // Optional (Standard: username)
|
||||||
|
"email": "string" // Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Registrierung erfolgreich!",
|
||||||
|
"user": {
|
||||||
|
"id": "string",
|
||||||
|
"username": "string",
|
||||||
|
"displayName": "string",
|
||||||
|
"email": "string",
|
||||||
|
"role": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fehlerantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Benutzername bereits vergeben!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benutzeranmeldung
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /auth/login`
|
||||||
|
|
||||||
|
**Beschreibung:** Meldet einen Benutzer an und erstellt eine Session.
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "string", // Erforderlich
|
||||||
|
"password": "string" // Erforderlich
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Anmeldung erfolgreich!",
|
||||||
|
"user": {
|
||||||
|
"id": "string",
|
||||||
|
"username": "string",
|
||||||
|
"displayName": "string",
|
||||||
|
"email": "string",
|
||||||
|
"role": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fehlerantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Ungültiger Benutzername oder Passwort!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialer Administrator
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /api/create-initial-admin`
|
||||||
|
|
||||||
|
**Beschreibung:** Erstellt einen initialen Admin-Benutzer, falls noch keiner existiert.
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "string", // Erforderlich
|
||||||
|
"password": "string", // Erforderlich
|
||||||
|
"displayName": "string", // Optional (Standard: username)
|
||||||
|
"email": "string" // Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Administrator wurde erfolgreich erstellt!",
|
||||||
|
"user": {
|
||||||
|
"id": "string",
|
||||||
|
"username": "string",
|
||||||
|
"displayName": "string",
|
||||||
|
"email": "string",
|
||||||
|
"role": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fehlerantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Es existiert bereits ein Administrator!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 (Steckdosen) zurück.
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"status": 0, // 0 = available, 1 = busy
|
||||||
|
"latestJob": {
|
||||||
|
// Job-Objekt oder null, wenn kein aktiver Job
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drucker hinzufügen (Admin)
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /api/printers`
|
||||||
|
|
||||||
|
**Beschreibung:** Fügt einen neuen Drucker hinzu.
|
||||||
|
|
||||||
|
**Erforderliche Rechte:** Admin
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"ipAddress": "string" // IP-Adresse der Tapo-Steckdose
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"status": 0, // 0 = available, 1 = busy
|
||||||
|
"latestJob": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drucker abrufen
|
||||||
|
|
||||||
|
**Endpunkt:** `GET /api/printers/{printerId}`
|
||||||
|
|
||||||
|
**Beschreibung:** Gibt die Details eines bestimmten Druckers zurück.
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"status": 0, // 0 = available, 1 = busy
|
||||||
|
"latestJob": {
|
||||||
|
// Job-Objekt oder null, wenn kein aktiver Job
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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",
|
||||||
|
"description": "string",
|
||||||
|
"ipAddress": "string", // IP-Adresse der Tapo-Steckdose
|
||||||
|
"status": 0 // 0 = available, 1 = busy
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"status": 0, // 0 = available, 1 = busy
|
||||||
|
"latestJob": {
|
||||||
|
// Job-Objekt oder null, wenn kein aktiver Job
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 60,
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": false,
|
||||||
|
"abortReason": null,
|
||||||
|
"remainingMinutes": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Druckauftrag erstellen
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /api/jobs`
|
||||||
|
|
||||||
|
**Beschreibung:** Erstellt einen neuen Druckauftrag.
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"printerId": "uuid-string",
|
||||||
|
"durationInMinutes": 60,
|
||||||
|
"comments": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 60,
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": false,
|
||||||
|
"abortReason": null,
|
||||||
|
"remainingMinutes": 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": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 60,
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": false,
|
||||||
|
"abortReason": null,
|
||||||
|
"remainingMinutes": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fehlerantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Nicht gefunden!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Druckauftrag Kommentare aktualisieren
|
||||||
|
|
||||||
|
**Endpunkt:** `PUT /api/jobs/{jobId}/comments`
|
||||||
|
|
||||||
|
**Beschreibung:** Aktualisiert die Kommentare eines Druckauftrags.
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"comments": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 60,
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": false,
|
||||||
|
"abortReason": null,
|
||||||
|
"remainingMinutes": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Druckauftrag abbrechen
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /api/jobs/{jobId}/abort`
|
||||||
|
|
||||||
|
**Beschreibung:** Bricht einen laufenden Druckauftrag ab.
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reason": "string" // Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 60,
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": true,
|
||||||
|
"abortReason": "string",
|
||||||
|
"remainingMinutes": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Druckauftrag vorzeitig beenden
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /api/jobs/{jobId}/finish`
|
||||||
|
|
||||||
|
**Beschreibung:** Beendet einen laufenden Druckauftrag vorzeitig.
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 45, // Tatsächliche Dauer bis zum Beenden
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": false,
|
||||||
|
"abortReason": null,
|
||||||
|
"remainingMinutes": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Druckauftrag verlängern
|
||||||
|
|
||||||
|
**Endpunkt:** `POST /api/jobs/{jobId}/extend`
|
||||||
|
|
||||||
|
**Beschreibung:** Verlängert die Laufzeit eines Druckauftrags.
|
||||||
|
|
||||||
|
**Request-Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"minutes": 30, // Zusätzliche Minuten
|
||||||
|
"hours": 0 // Zusätzliche Stunden (optional)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 90, // Aktualisierte Gesamtdauer
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": false,
|
||||||
|
"abortReason": null,
|
||||||
|
"remainingMinutes": 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,
|
||||||
|
"job_status": "active", // active, completed
|
||||||
|
"socket_status": "busy" // busy, available
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status eines Druckauftrags abrufen
|
||||||
|
|
||||||
|
**Endpunkt:** `GET /api/job/{jobId}/status`
|
||||||
|
|
||||||
|
**Beschreibung:** Gibt detaillierte Statusinformationen zu einem Druckauftrag zurück.
|
||||||
|
|
||||||
|
**Erfolgsantwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"job": {
|
||||||
|
"id": "uuid-string",
|
||||||
|
"socketId": "uuid-string",
|
||||||
|
"userId": "uuid-string",
|
||||||
|
"startAt": "string (ISO 8601)",
|
||||||
|
"durationInMinutes": 60,
|
||||||
|
"comments": "string",
|
||||||
|
"aborted": false,
|
||||||
|
"abortReason": null,
|
||||||
|
"remainingMinutes": 30
|
||||||
|
},
|
||||||
|
"status": "active", // active, completed, aborted
|
||||||
|
"socketStatus": "busy", // busy, available
|
||||||
|
"remainingMinutes": 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 |
|
213
backend/docs/PROJEKTDOKUMENTATION.md
Normal file
213
backend/docs/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
|
||||||
|
```
|
185
backend/docs/README.md
Normal file
185
backend/docs/README.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
- Lokales Authentifizierungssystem (Offline-fähig)
|
||||||
|
- 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
|
||||||
|
- **SQLite**: Integrierte Datenbank (kann für Produktion durch PostgreSQL ersetzt werden)
|
||||||
|
- **PyP100**: Python-Bibliothek zur 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
|
||||||
|
|
||||||
|
- `POST /auth/register`: Neuen Benutzer registrieren
|
||||||
|
- `POST /auth/login`: Benutzer anmelden
|
||||||
|
- `POST /auth/logout`: Abmelden und Session beenden
|
||||||
|
- `POST /api/create-initial-admin`: Initialen Administrator erstellen
|
||||||
|
- `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)
|
||||||
|
- username (String, Unique)
|
||||||
|
- password_hash (String)
|
||||||
|
- 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 ein lokales Authentifizierungssystem mit Passwort-Hashing
|
||||||
|
- 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
|
||||||
|
- Der erste registrierte Benutzer wird automatisch zum Administrator
|
||||||
|
|
||||||
|
## 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_PATH`: Pfad zur Datenbank (Standard: SQLite-Datenbank im Instance-Verzeichnis)
|
||||||
|
- `TAPO_USERNAME`: Benutzername für die Tapo-Steckdosen
|
||||||
|
- `TAPO_PASSWORD`: Passwort für die Tapo-Steckdosen
|
||||||
|
- `PRINTERS`: JSON-Objekt mit der Zuordnung von Drucker-Namen zu IP-Adressen der Steckdosen im Format: `{"Printer 1": {"ip": "192.168.1.100"}, "Printer 2": {"ip": "192.168.1.101"}, ...}`
|
||||||
|
|
||||||
|
## 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.
|
89
backend/log.txt
Normal file
89
backend/log.txt
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
root@raspberrypi:/home/user/Projektarbeit-MYP/backend# python3 app.py
|
||||||
|
[2025-03-24 09:38:15,229] INFO in app: MYP Backend starting up
|
||||||
|
[2025-03-24 09:38:15,338] INFO in app: Initialisiere Drucker aus Umgebungsvariablen
|
||||||
|
[2025-03-24 09:38:15,353] INFO in app: Neuer Drucker angelegt: Printer 1 mit IP 192.168.0.100
|
||||||
|
[2025-03-24 09:38:16,197] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:16,197] INFO in app: Neue Steckdose mit IP 192.168.0.100 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:16,209] INFO in app: Neuer Drucker angelegt: Printer 2 mit IP 192.168.0.101
|
||||||
|
[2025-03-24 09:38:16,521] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:16,522] INFO in app: Neue Steckdose mit IP 192.168.0.101 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:16,536] INFO in app: Neuer Drucker angelegt: Printer 3 mit IP 192.168.0.102
|
||||||
|
[2025-03-24 09:38:17,082] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:17,083] INFO in app: Neue Steckdose mit IP 192.168.0.102 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:17,096] INFO in app: Neuer Drucker angelegt: Printer 4 mit IP 192.168.0.103
|
||||||
|
[2025-03-24 09:38:18,248] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:18,249] INFO in app: Neue Steckdose mit IP 192.168.0.103 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:18,263] INFO in app: Neuer Drucker angelegt: Printer 5 mit IP 192.168.0.104
|
||||||
|
[2025-03-24 09:38:18,635] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:18,636] INFO in app: Neue Steckdose mit IP 192.168.0.104 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:18,650] INFO in app: Neuer Drucker angelegt: Printer 6 mit IP 192.168.0.106
|
||||||
|
[2025-03-24 09:38:21,004] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.106: HTTPConnectionPool(host='192.168.0.106', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7fb0b1dd90>, 'Connection to 192.168.0.106 timed out. (connect timeout=2)'))
|
||||||
|
[2025-03-24 09:38:21,006] INFO in app: Neue Steckdose mit IP 192.168.0.106 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:21,007] INFO in app: Starte Hintergrund-Thread für Job-Überprüfung und Steckdosen-Monitoring
|
||||||
|
[2025-03-24 09:38:21,008] INFO in app: Hintergrund-Thread für Job-Überprüfung gestartet
|
||||||
|
[2025-03-24 09:38:21,014] INFO in app: 0 abgelaufene Jobs überprüft, 0 Steckdosen aktualisiert.
|
||||||
|
* Serving Flask app 'app'
|
||||||
|
* Debug mode: on
|
||||||
|
[2025-03-24 09:38:21,023] INFO in app: Überprüfe Verbindungsstatus von 6 Steckdosen
|
||||||
|
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||||
|
* Running on all addresses (0.0.0.0)
|
||||||
|
* Running on http://127.0.0.1:5000
|
||||||
|
* Running on http://192.168.0.105:5000
|
||||||
|
Press CTRL+C to quit
|
||||||
|
* Restarting with stat
|
||||||
|
[2025-03-24 09:38:21,810] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:21,826] INFO in app: Verbindungsstatus für Steckdose 80c65076-acdb-4448-ac6e-05a44b35f5b2 geändert: offline
|
||||||
|
[2025-03-24 09:38:21,845] WARNING in app: Steckdose Printer 1 (192.168.0.100) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:21,913] INFO in app: MYP Backend starting up
|
||||||
|
[2025-03-24 09:38:21,968] INFO in app: Initialisiere Drucker aus Umgebungsvariablen
|
||||||
|
[2025-03-24 09:38:21,969] INFO in app: Drucker mit IP 192.168.0.100 existiert bereits in der Datenbank
|
||||||
|
[2025-03-24 09:38:22,109] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:22,120] INFO in app: Verbindungsstatus für Steckdose 19e70cd5-5fdb-439b-80e3-807015c7cb15 geändert: offline
|
||||||
|
[2025-03-24 09:38:22,134] WARNING in app: Steckdose Printer 2 (192.168.0.101) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:22,666] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:22,667] INFO in app: Steckdose mit IP 192.168.0.100 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:22,668] INFO in app: Drucker mit IP 192.168.0.101 existiert bereits in der Datenbank
|
||||||
|
[2025-03-24 09:38:22,806] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:22,819] INFO in app: Verbindungsstatus für Steckdose 7cdc29a8-3593-4666-8419-070914c6d6c5 geändert: offline
|
||||||
|
[2025-03-24 09:38:22,831] WARNING in app: Steckdose Printer 3 (192.168.0.102) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:23,222] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:23,223] INFO in app: Steckdose mit IP 192.168.0.101 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:23,223] INFO in app: Drucker mit IP 192.168.0.102 existiert bereits in der Datenbank
|
||||||
|
[2025-03-24 09:38:23,228] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:23,243] INFO in app: Verbindungsstatus für Steckdose 69be8092-0eea-4797-a940-51bdec244cf7 geändert: offline
|
||||||
|
[2025-03-24 09:38:23,256] WARNING in app: Steckdose Printer 4 (192.168.0.103) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:23,458] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:23,476] INFO in app: Verbindungsstatus für Steckdose 90caa30e-adaf-44ec-a680-6beea72a570a geändert: offline
|
||||||
|
[2025-03-24 09:38:23,489] WARNING in app: Steckdose Printer 5 (192.168.0.104) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:23,492] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:23,493] INFO in app: Steckdose mit IP 192.168.0.102 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:23,493] INFO in app: Drucker mit IP 192.168.0.103 existiert bereits in der Datenbank
|
||||||
|
[2025-03-24 09:38:24,058] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:24,058] INFO in app: Steckdose mit IP 192.168.0.103 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:24,059] INFO in app: Drucker mit IP 192.168.0.104 existiert bereits in der Datenbank
|
||||||
|
[2025-03-24 09:38:24,610] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:24,611] INFO in app: Steckdose mit IP 192.168.0.104 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:24,612] INFO in app: Drucker mit IP 192.168.0.106 existiert bereits in der Datenbank
|
||||||
|
[2025-03-24 09:38:26,344] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.106: HTTPConnectionPool(host='192.168.0.106', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7fafc91790>, 'Connection to 192.168.0.106 timed out. (connect timeout=2)'))
|
||||||
|
[2025-03-24 09:38:26,357] INFO in app: Verbindungsstatus für Steckdose 2b6b9831-e4c1-4f60-8107-69cbc8b58e2c geändert: offline
|
||||||
|
[2025-03-24 09:38:26,370] WARNING in app: Steckdose Printer 6 (192.168.0.106) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:26,371] INFO in app: Verbindungsüberprüfung abgeschlossen: 0 online, 6 offline, 0 übersprungen
|
||||||
|
[2025-03-24 09:38:26,371] INFO in app: Nächste Socket-Überprüfung in 120 Sekunden
|
||||||
|
[2025-03-24 09:38:26,775] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.106: HTTPConnectionPool(host='192.168.0.106', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7f8214df50>, 'Connection to 192.168.0.106 timed out. (connect timeout=2)'))
|
||||||
|
[2025-03-24 09:38:26,776] INFO in app: Steckdose mit IP 192.168.0.106 wurde beim Start ausgeschaltet
|
||||||
|
[2025-03-24 09:38:26,776] INFO in app: Starte Hintergrund-Thread für Job-Überprüfung und Steckdosen-Monitoring
|
||||||
|
[2025-03-24 09:38:26,777] INFO in app: Hintergrund-Thread für Job-Überprüfung gestartet
|
||||||
|
[2025-03-24 09:38:26,780] INFO in app: 0 abgelaufene Jobs überprüft, 0 Steckdosen aktualisiert.
|
||||||
|
[2025-03-24 09:38:26,784] INFO in app: Überprüfe Verbindungsstatus von 6 Steckdosen
|
||||||
|
* Debugger is active!
|
||||||
|
* Debugger PIN: 101-484-383
|
||||||
|
[2025-03-24 09:38:27,279] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:27,280] WARNING in app: Steckdose Printer 1 (192.168.0.100) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:27,719] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:27,720] WARNING in app: Steckdose Printer 2 (192.168.0.101) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:28,073] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:28,074] WARNING in app: Steckdose Printer 3 (192.168.0.102) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:28,887] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:28,887] WARNING in app: Steckdose Printer 4 (192.168.0.103) ist nicht erreichbar
|
||||||
|
[2025-03-24 09:38:29,312] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
||||||
|
[2025-03-24 09:38:29,312] WARNING in app: Steckdose Printer 5 (192.168.0.104) ist nicht erreichbar
|
1
backend/migrations/README
Normal file
1
backend/migrations/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
50
backend/migrations/alembic.ini
Normal file
50
backend/migrations/alembic.ini
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic,flask_migrate
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
113
backend/migrations/env.py
Normal file
113
backend/migrations/env.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata():
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas[None]
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
conf_args = current_app.extensions['migrate'].configure_args
|
||||||
|
if conf_args.get("process_revision_directives") is None:
|
||||||
|
conf_args["process_revision_directives"] = process_revision_directives
|
||||||
|
|
||||||
|
connectable = get_engine()
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
**conf_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
24
backend/migrations/script.py.mako
Normal file
24
backend/migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
42
backend/migrations/versions/add_waiting_approval.py
Normal file
42
backend/migrations/versions/add_waiting_approval.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Add waiting_approval column to job table
|
||||||
|
|
||||||
|
Revision ID: add_waiting_approval
|
||||||
|
Revises: af3faaa3844c
|
||||||
|
Create Date: 2025-03-12 14:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_waiting_approval'
|
||||||
|
down_revision = 'af3faaa3844c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Füge die neue Spalte waiting_approval zur job-Tabelle hinzu
|
||||||
|
with op.batch_alter_table('job', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('waiting_approval', sa.Integer(), server_default='0', nullable=False))
|
||||||
|
|
||||||
|
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
||||||
|
try:
|
||||||
|
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('waiting_approval', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Migration für print_job-Tabelle übersprungen: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Entferne die waiting_approval-Spalte aus der job-Tabelle
|
||||||
|
with op.batch_alter_table('job', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('waiting_approval')
|
||||||
|
|
||||||
|
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
||||||
|
try:
|
||||||
|
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('waiting_approval')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Downgrade für print_job-Tabelle übersprungen: {e}")
|
81
backend/migrations/versions/af3faaa3844c_.py
Normal file
81
backend/migrations/versions/af3faaa3844c_.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: af3faaa3844c
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-03-11 11:16:04.961964
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'af3faaa3844c'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('printer',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=False),
|
||||||
|
sa.Column('status', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('ip_address', sa.String(length=15), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('printer', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_printer_name'), ['name'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('user',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('display_name', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('email', sa.String(length=120), nullable=True),
|
||||||
|
sa.Column('role', sa.String(length=20), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('print_job',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('printer_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('user_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('start_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('duration_in_minutes', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('comments', sa.Text(), nullable=True),
|
||||||
|
sa.Column('aborted', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('abort_reason', sa.Text(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['printer_id'], ['printer.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('session',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('user_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('session')
|
||||||
|
op.drop_table('print_job')
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_user_username'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_user_email'))
|
||||||
|
|
||||||
|
op.drop_table('user')
|
||||||
|
with op.batch_alter_table('printer', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_printer_name'))
|
||||||
|
|
||||||
|
op.drop_table('printer')
|
||||||
|
# ### end Alembic commands ###
|
7
backend/requirements.txt
Normal file
7
backend/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
flask==2.3.3
|
||||||
|
flask-cors==4.0.0
|
||||||
|
pyjwt==2.8.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
werkzeug==2.3.7
|
||||||
|
gunicorn==21.2.0
|
||||||
|
PyP100==0.0.19
|
12068
backend/static/css/bootstrap.css
vendored
Normal file
12068
backend/static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6314
backend/static/js/bootstrap.bundle.js
vendored
Normal file
6314
backend/static/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
169
backend/templates/base.html
Normal file
169
backend/templates/base.html
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}MYP API Tester{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.css') }}">
|
||||||
|
<style>
|
||||||
|
.sidebar {
|
||||||
|
min-height: calc(100vh - 56px);
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.api-response {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.nav-link.active {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">MYP API Tester</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'printers' %}active{% endif %}" href="/admin/printers">Drucker</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'jobs' %}active{% endif %}" href="/admin/jobs">Druckaufträge</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'users' %}active{% endif %}" href="/admin/users">Benutzer</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'stats' %}active{% endif %}" href="/admin/stats">Statistiken</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
{% if current_user %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
{{ current_user.username }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><a class="dropdown-item" href="/logout">Abmelden</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/login">Anmelden</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid py-3">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
function formatJson(jsonString) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(jsonString);
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Format all response areas
|
||||||
|
document.querySelectorAll('.api-response').forEach(function(el) {
|
||||||
|
if (el.textContent) {
|
||||||
|
el.textContent = formatJson(el.textContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listener to show response areas
|
||||||
|
document.querySelectorAll('.api-form').forEach(function(form) {
|
||||||
|
form.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const url = this.getAttribute('data-url');
|
||||||
|
const method = this.getAttribute('data-method') || 'GET';
|
||||||
|
const responseArea = document.getElementById(this.getAttribute('data-response'));
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
// Try to parse as JSON if it looks like JSON
|
||||||
|
if (value.trim().startsWith('{') || value.trim().startsWith('[')) {
|
||||||
|
data[key] = JSON.parse(value);
|
||||||
|
} else {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method !== 'GET' && method !== 'HEAD') {
|
||||||
|
options.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseArea.textContent = 'Sending request...';
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
const responseText = await response.text();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formatted = formatJson(responseText);
|
||||||
|
responseArea.textContent = formatted;
|
||||||
|
} catch (e) {
|
||||||
|
responseArea.textContent = responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasAttribute('data-reload') && response.ok) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
responseArea.textContent = 'Error: ' + err.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
304
backend/templates/dashboard.html
Normal file
304
backend/templates/dashboard.html
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - MYP API Tester{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Willkommen, {{ current_user.display_name }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Benutzerdetails:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>ID:</strong> {{ current_user.id }}</li>
|
||||||
|
<li><strong>Benutzername:</strong> {{ current_user.username }}</li>
|
||||||
|
<li><strong>E-Mail:</strong> {{ current_user.email or "Nicht angegeben" }}</li>
|
||||||
|
<li><strong>Rolle:</strong> {{ current_user.role }}</li>
|
||||||
|
</ul>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/admin/printers" class="btn btn-primary me-2">Drucker verwalten</a>
|
||||||
|
<a href="/admin/jobs" class="btn btn-success me-2">Druckaufträge verwalten</a>
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<a href="/admin/users" class="btn btn-info me-2">Benutzer verwalten</a>
|
||||||
|
<a href="/admin/stats" class="btn btn-secondary">Statistiken</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Aktive Druckaufträge</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form mb-3" data-url="/api/jobs" data-method="GET" data-response="jobsResponse">
|
||||||
|
<button type="submit" class="btn btn-primary">Aktualisieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="activeJobsContainer">
|
||||||
|
<div class="alert alert-info">Lade Druckaufträge...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none">
|
||||||
|
<h6>API-Antwort:</h6>
|
||||||
|
<pre class="api-response" id="jobsResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Verfügbare Drucker</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form mb-3" data-url="/api/printers" data-method="GET" data-response="printersResponse">
|
||||||
|
<button type="submit" class="btn btn-primary">Aktualisieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="availablePrintersContainer">
|
||||||
|
<div class="alert alert-info">Lade Drucker...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none">
|
||||||
|
<h6>API-Antwort:</h6>
|
||||||
|
<pre class="api-response" id="printersResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job freischalten Modal -->
|
||||||
|
<div class="modal fade" id="approveJobModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Druckauftrag freischalten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Möchten Sie diesen Druckauftrag jetzt freischalten und starten?</p>
|
||||||
|
<p><strong>Hinweis:</strong> Der Drucker muss verfügbar sein, damit der Auftrag gestartet werden kann.</p>
|
||||||
|
<form id="approveJobForm" class="api-form" data-method="POST" data-response="approveJobResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="approveJobId" name="jobId">
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="approveJobResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="approveJobForm" class="btn btn-success">Freischalten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Aufträge und Drucker laden
|
||||||
|
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
||||||
|
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
||||||
|
|
||||||
|
// Tabellen aktualisieren, wenn Daten geladen werden
|
||||||
|
const jobsResponse = document.getElementById('jobsResponse');
|
||||||
|
const printersResponse = document.getElementById('printersResponse');
|
||||||
|
|
||||||
|
// Observer für Jobs
|
||||||
|
const jobsObserver = new MutationObserver(function(mutations) {
|
||||||
|
try {
|
||||||
|
const jobs = JSON.parse(jobsResponse.textContent);
|
||||||
|
updateActiveJobs(jobs);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Parsen der Auftrags-Daten:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jobsObserver.observe(jobsResponse, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// Observer für Drucker
|
||||||
|
const printersObserver = new MutationObserver(function(mutations) {
|
||||||
|
try {
|
||||||
|
const printers = JSON.parse(printersResponse.textContent);
|
||||||
|
updateAvailablePrinters(printers);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Parsen der Drucker-Daten:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
printersObserver.observe(printersResponse, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// Approve-Modal vorbereiten
|
||||||
|
document.getElementById('approveJobModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const jobId = button.getAttribute('data-job-id');
|
||||||
|
|
||||||
|
document.getElementById('approveJobId').value = jobId;
|
||||||
|
document.getElementById('approveJobForm').setAttribute('data-url', `/api/jobs/${jobId}/approve`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Automatische Aktualisierung alle 60 Sekunden
|
||||||
|
setInterval(() => {
|
||||||
|
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
||||||
|
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
||||||
|
}, 60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateActiveJobs(jobs) {
|
||||||
|
const container = document.getElementById('activeJobsContainer');
|
||||||
|
|
||||||
|
// Filter für aktive und wartende Jobs
|
||||||
|
const activeJobs = jobs.filter(job => !job.aborted && job.remainingMinutes > 0 && !job.waitingApproval);
|
||||||
|
const waitingJobs = jobs.filter(job => !job.aborted && job.waitingApproval);
|
||||||
|
|
||||||
|
if (activeJobs.length === 0 && waitingJobs.length === 0) {
|
||||||
|
container.innerHTML = '<div class="alert alert-info">Keine aktiven Druckaufträge vorhanden.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Aktive Jobs anzeigen
|
||||||
|
if (activeJobs.length > 0) {
|
||||||
|
html += '<h6 class="mt-3">Laufende Aufträge</h6>';
|
||||||
|
html += '<div class="list-group mb-3">';
|
||||||
|
|
||||||
|
activeJobs.forEach(job => {
|
||||||
|
// Prozentsatz der abgelaufenen Zeit berechnen
|
||||||
|
const totalDuration = job.durationInMinutes;
|
||||||
|
const elapsed = totalDuration - job.remainingMinutes;
|
||||||
|
const percentage = Math.round((elapsed / totalDuration) * 100);
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<strong>Job ${job.id.substring(0, 8)}...</strong> (${job.durationInMinutes} Min)
|
||||||
|
<div class="small text-muted">Verbleibend: ${job.remainingMinutes} Min</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-warning">Aktiv</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-2" style="height: 10px;">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: ${percentage}%;"
|
||||||
|
aria-valuenow="${percentage}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
${percentage}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wartende Jobs anzeigen
|
||||||
|
if (waitingJobs.length > 0) {
|
||||||
|
html += '<h6 class="mt-3">Wartende Aufträge</h6>';
|
||||||
|
html += '<div class="list-group">';
|
||||||
|
|
||||||
|
waitingJobs.forEach(job => {
|
||||||
|
html += `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<strong>Job ${job.id.substring(0, 8)}...</strong> (${job.durationInMinutes} Min)
|
||||||
|
<div class="small text-muted">Drucker: ${job.socketId.substring(0, 8)}...</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-info">Wartet</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-success"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#approveJobModal"
|
||||||
|
data-job-id="${job.id}">
|
||||||
|
Freischalten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAvailablePrinters(printers) {
|
||||||
|
const container = document.getElementById('availablePrintersContainer');
|
||||||
|
|
||||||
|
// Filter für verfügbare Drucker
|
||||||
|
const availablePrinters = printers.filter(printer => printer.status === 0);
|
||||||
|
|
||||||
|
if (availablePrinters.length === 0) {
|
||||||
|
container.innerHTML = '<div class="alert alert-warning">Keine verfügbaren Drucker gefunden.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="list-group">';
|
||||||
|
|
||||||
|
availablePrinters.forEach(printer => {
|
||||||
|
html += `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>${printer.name}</strong>
|
||||||
|
<div class="small text-muted">${printer.description}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-success">Verfügbar</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="/admin/jobs" class="btn btn-sm btn-primary">Auftrag erstellen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Gesamtstatistik hinzufügen
|
||||||
|
const busyPrinters = printers.filter(printer => printer.status === 1).length;
|
||||||
|
const totalPrinters = printers.length;
|
||||||
|
|
||||||
|
if (totalPrinters > 0) {
|
||||||
|
const statsHtml = `
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small>Verfügbar: ${availablePrinters.length} / ${totalPrinters}</small>
|
||||||
|
<small>Belegt: ${busyPrinters} / ${totalPrinters}</small>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-1" style="height: 5px;">
|
||||||
|
<div class="progress-bar bg-success" style="width: ${(availablePrinters.length / totalPrinters) * 100}%"></div>
|
||||||
|
<div class="progress-bar bg-warning" style="width: ${(busyPrinters / totalPrinters) * 100}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML += statsHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
443
backend/templates/jobs.html
Normal file
443
backend/templates/jobs.html
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Druckaufträge - MYP API Tester{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h4 class="mb-0">Druckaufträge verwalten</h4>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newJobForm">
|
||||||
|
Neuen Auftrag erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="newJobForm">
|
||||||
|
<div class="card-body border-bottom">
|
||||||
|
<form class="api-form" data-url="/api/jobs" data-method="POST" data-response="createJobResponse" data-reload="true">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="jobPrinterId" class="form-label">Drucker</label>
|
||||||
|
<select class="form-control" id="jobPrinterId" name="printerId" required>
|
||||||
|
<option value="">Drucker auswählen...</option>
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="jobDuration" class="form-label">Dauer (Minuten)</label>
|
||||||
|
<input type="number" class="form-control" id="jobDuration" name="durationInMinutes" min="1" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="jobComments" class="form-label">Kommentare</label>
|
||||||
|
<textarea class="form-control" id="jobComments" name="comments" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="allowQueuedJobs" name="allowQueuedJobs" value="true">
|
||||||
|
<label class="form-check-label" for="allowQueuedJobs">
|
||||||
|
Auftrag in Warteschlange erlauben (wenn Drucker belegt ist)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">Auftrag erstellen</button>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="createJobResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form mb-3" data-url="/api/jobs" data-method="GET" data-response="jobsResponse">
|
||||||
|
<button type="submit" class="btn btn-primary">Aufträge aktualisieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Drucker</th>
|
||||||
|
<th>Benutzer</th>
|
||||||
|
<th>Start</th>
|
||||||
|
<th>Dauer (Min)</th>
|
||||||
|
<th>Verbleibend (Min)</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Kommentare</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="jobsTableBody">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h6>API-Antwort:</h6>
|
||||||
|
<pre class="api-response" id="jobsResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job abbrechen Modal -->
|
||||||
|
<div class="modal fade" id="abortJobModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Auftrag abbrechen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Möchten Sie den Auftrag wirklich abbrechen?</p>
|
||||||
|
<form id="abortJobForm" class="api-form" data-method="POST" data-response="abortJobResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="abortJobId" name="jobId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="abortReason" class="form-label">Abbruchgrund</label>
|
||||||
|
<textarea class="form-control" id="abortReason" name="reason" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="abortJobResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="abortJobForm" class="btn btn-danger">Auftrag abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job beenden Modal -->
|
||||||
|
<div class="modal fade" id="finishJobModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Auftrag beenden</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Möchten Sie den Auftrag als beendet markieren?</p>
|
||||||
|
<form id="finishJobForm" class="api-form" data-method="POST" data-response="finishJobResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="finishJobId" name="jobId">
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="finishJobResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="finishJobForm" class="btn btn-success">Auftrag beenden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job verlängern Modal -->
|
||||||
|
<div class="modal fade" id="extendJobModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Auftrag verlängern</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="extendJobForm" class="api-form" data-method="POST" data-response="extendJobResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="extendJobId" name="jobId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="extendHours" class="form-label">Stunden</label>
|
||||||
|
<input type="number" class="form-control" id="extendHours" name="hours" min="0" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="extendMinutes" class="form-label">Minuten</label>
|
||||||
|
<input type="number" class="form-control" id="extendMinutes" name="minutes" min="0" max="59" value="30">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="extendJobResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="extendJobForm" class="btn btn-primary">Auftrag verlängern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job Kommentare bearbeiten Modal -->
|
||||||
|
<div class="modal fade" id="editCommentsModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Kommentare bearbeiten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editCommentsForm" class="api-form" data-method="PUT" data-response="editCommentsResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="editCommentsJobId" name="jobId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editJobComments" class="form-label">Kommentare</label>
|
||||||
|
<textarea class="form-control" id="editJobComments" name="comments" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="editCommentsResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="editCommentsForm" class="btn btn-primary">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job freischalten Modal -->
|
||||||
|
<div class="modal fade" id="approveJobModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Druckauftrag freischalten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Möchten Sie diesen Druckauftrag jetzt freischalten und starten?</p>
|
||||||
|
<p><strong>Hinweis:</strong> Der Drucker muss verfügbar sein, damit der Auftrag gestartet werden kann.</p>
|
||||||
|
<form id="approveJobForm" class="api-form" data-method="POST" data-response="approveJobResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="approveJobId" name="jobId">
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="approveJobResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="approveJobForm" class="btn btn-success">Freischalten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Drucker für Dropdown laden
|
||||||
|
loadPrinters();
|
||||||
|
|
||||||
|
// Aufträge laden
|
||||||
|
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
||||||
|
|
||||||
|
// Tabelle aktualisieren, wenn Aufträge geladen werden
|
||||||
|
const jobsResponse = document.getElementById('jobsResponse');
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
try {
|
||||||
|
const jobs = JSON.parse(jobsResponse.textContent);
|
||||||
|
updateJobsTable(jobs);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Parsen der Auftrags-Daten:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(jobsResponse, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// Abort-Modal vorbereiten
|
||||||
|
document.getElementById('abortJobModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const jobId = button.getAttribute('data-job-id');
|
||||||
|
|
||||||
|
document.getElementById('abortJobId').value = jobId;
|
||||||
|
document.getElementById('abortJobForm').setAttribute('data-url', `/api/jobs/${jobId}/abort`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finish-Modal vorbereiten
|
||||||
|
document.getElementById('finishJobModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const jobId = button.getAttribute('data-job-id');
|
||||||
|
|
||||||
|
document.getElementById('finishJobId').value = jobId;
|
||||||
|
document.getElementById('finishJobForm').setAttribute('data-url', `/api/jobs/${jobId}/finish`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extend-Modal vorbereiten
|
||||||
|
document.getElementById('extendJobModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const jobId = button.getAttribute('data-job-id');
|
||||||
|
|
||||||
|
document.getElementById('extendJobId').value = jobId;
|
||||||
|
document.getElementById('extendJobForm').setAttribute('data-url', `/api/jobs/${jobId}/extend`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit-Comments-Modal vorbereiten
|
||||||
|
document.getElementById('editCommentsModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const jobId = button.getAttribute('data-job-id');
|
||||||
|
const comments = button.getAttribute('data-job-comments');
|
||||||
|
|
||||||
|
document.getElementById('editCommentsJobId').value = jobId;
|
||||||
|
document.getElementById('editCommentsForm').setAttribute('data-url', `/api/jobs/${jobId}/comments`);
|
||||||
|
document.getElementById('editJobComments').value = comments || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Approve-Modal vorbereiten
|
||||||
|
document.getElementById('approveJobModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const jobId = button.getAttribute('data-job-id');
|
||||||
|
|
||||||
|
document.getElementById('approveJobId').value = jobId;
|
||||||
|
document.getElementById('approveJobForm').setAttribute('data-url', `/api/jobs/${jobId}/approve`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadPrinters() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/printers');
|
||||||
|
const printers = await response.json();
|
||||||
|
|
||||||
|
const selectElement = document.getElementById('jobPrinterId');
|
||||||
|
selectElement.innerHTML = '<option value="">Drucker auswählen...</option>';
|
||||||
|
|
||||||
|
// Drucker anzeigen (alle, da man jetzt auch für belegte Drucker Jobs erstellen kann)
|
||||||
|
printers.forEach(printer => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = printer.id;
|
||||||
|
|
||||||
|
// Status-Information zum Drucker hinzufügen
|
||||||
|
const statusText = printer.status === 0 ? '(Verfügbar)' : '(Belegt)';
|
||||||
|
option.textContent = `${printer.name} - ${printer.description} ${statusText}`;
|
||||||
|
|
||||||
|
// Belegte Drucker visuell unterscheiden
|
||||||
|
if (printer.status !== 0) {
|
||||||
|
option.classList.add('text-muted');
|
||||||
|
}
|
||||||
|
|
||||||
|
selectElement.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hinweis auf die Checkbox für Warteschlange anzeigen oder verstecken
|
||||||
|
const allowQueuedJobsCheckbox = document.getElementById('allowQueuedJobs');
|
||||||
|
const queueCheckboxContainer = allowQueuedJobsCheckbox.closest('.form-check');
|
||||||
|
|
||||||
|
// Prüfen, ob es belegte Drucker gibt
|
||||||
|
const hasBusyPrinters = printers.some(printer => printer.status !== 0);
|
||||||
|
queueCheckboxContainer.style.display = hasBusyPrinters ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Event-Listener für die Druckerauswahl hinzufügen
|
||||||
|
selectElement.addEventListener('change', function() {
|
||||||
|
const selectedPrinterId = this.value;
|
||||||
|
const selectedPrinter = printers.find(p => p.id === selectedPrinterId);
|
||||||
|
|
||||||
|
if (selectedPrinter && selectedPrinter.status !== 0) {
|
||||||
|
// Wenn ein belegter Drucker ausgewählt wird, Checkbox für Warteschlange anzeigen
|
||||||
|
queueCheckboxContainer.style.display = 'block';
|
||||||
|
allowQueuedJobsCheckbox.checked = true;
|
||||||
|
} else if (selectedPrinter && selectedPrinter.status === 0) {
|
||||||
|
// Wenn ein verfügbarer Drucker ausgewählt wird, Checkbox für Warteschlange verstecken
|
||||||
|
allowQueuedJobsCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Laden der Drucker:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJobsTable(jobs) {
|
||||||
|
const tableBody = document.getElementById('jobsTableBody');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
jobs.forEach(job => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
const startDate = new Date(job.startAt);
|
||||||
|
const formattedStart = startDate.toLocaleString();
|
||||||
|
|
||||||
|
const isActive = !job.aborted && job.remainingMinutes > 0 && !job.waitingApproval;
|
||||||
|
const isWaiting = !job.aborted && job.waitingApproval;
|
||||||
|
|
||||||
|
let statusText = '';
|
||||||
|
let statusClass = '';
|
||||||
|
|
||||||
|
if (job.aborted) {
|
||||||
|
statusText = 'Abgebrochen';
|
||||||
|
statusClass = 'text-danger';
|
||||||
|
} else if (job.waitingApproval) {
|
||||||
|
statusText = 'Wartet auf Freischaltung';
|
||||||
|
statusClass = 'text-info';
|
||||||
|
} else if (job.remainingMinutes <= 0) {
|
||||||
|
statusText = 'Abgeschlossen';
|
||||||
|
statusClass = 'text-success';
|
||||||
|
} else {
|
||||||
|
statusText = 'Aktiv';
|
||||||
|
statusClass = 'text-warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige die verbleibende Zeit an
|
||||||
|
const remainingTime = job.waitingApproval ? '-' : job.remainingMinutes;
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${job.id}</td>
|
||||||
|
<td>${job.printerId}</td>
|
||||||
|
<td>${job.userId}</td>
|
||||||
|
<td>${formattedStart}</td>
|
||||||
|
<td>${job.durationInMinutes}</td>
|
||||||
|
<td>${remainingTime}</td>
|
||||||
|
<td><span class="${statusClass}">${statusText}</span></td>
|
||||||
|
<td>${job.comments || '-'}</td>
|
||||||
|
<td>
|
||||||
|
${isActive ? `
|
||||||
|
<button type="button" class="btn btn-sm btn-danger mb-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#abortJobModal"
|
||||||
|
data-job-id="${job.id}">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-success mb-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#finishJobModal"
|
||||||
|
data-job-id="${job.id}">
|
||||||
|
Beenden
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary mb-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#extendJobModal"
|
||||||
|
data-job-id="${job.id}">
|
||||||
|
Verlängern
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${isWaiting ? `
|
||||||
|
<button type="button" class="btn btn-sm btn-success mb-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#approveJobModal"
|
||||||
|
data-job-id="${job.id}">
|
||||||
|
Freischalten
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger mb-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#abortJobModal"
|
||||||
|
data-job-id="${job.id}">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary mb-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editCommentsModal"
|
||||||
|
data-job-id="${job.id}"
|
||||||
|
data-job-comments="${job.comments || ''}">
|
||||||
|
Kommentare
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
37
backend/templates/login.html
Normal file
37
backend/templates/login.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Anmelden - MYP API Tester{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Anmelden</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form" data-url="/auth/login" data-method="POST" data-response="loginResponse">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Benutzername</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Passwort</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p>Noch kein Konto? <a href="/register">Registrieren</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<h5>Antwort:</h5>
|
||||||
|
<pre class="api-response" id="loginResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
280
backend/templates/printers.html
Normal file
280
backend/templates/printers.html
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Drucker - MYP API Tester{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h4 class="mb-0">Drucker verwalten</h4>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newPrinterForm">
|
||||||
|
Neuen Drucker hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="newPrinterForm">
|
||||||
|
<div class="card-body border-bottom">
|
||||||
|
<form class="api-form" data-url="/api/printers" data-method="POST" data-response="createPrinterResponse" data-reload="true">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="printerName" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control" id="printerName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="printerDescription" class="form-label">Beschreibung</label>
|
||||||
|
<textarea class="form-control" id="printerDescription" name="description" rows="3" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="printerStatus" class="form-label">Status</label>
|
||||||
|
<select class="form-control" id="printerStatus" name="status">
|
||||||
|
<option value="0">Verfügbar (0)</option>
|
||||||
|
<option value="1">Besetzt (1)</option>
|
||||||
|
<option value="2">Wartung (2)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="printerIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label>
|
||||||
|
<input type="text" class="form-control" id="printerIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">Drucker erstellen</button>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="createPrinterResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form mb-3" data-url="/api/printers" data-method="GET" data-response="printersResponse">
|
||||||
|
<button type="submit" class="btn btn-primary">Drucker aktualisieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Beschreibung</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>IP-Adresse</th>
|
||||||
|
<th>Aktueller Job</th>
|
||||||
|
<th>Wartende Jobs</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="printersTableBody">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h6>API-Antwort:</h6>
|
||||||
|
<pre class="api-response" id="printersResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drucker bearbeiten Modal -->
|
||||||
|
<div class="modal fade" id="editPrinterModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Drucker bearbeiten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editPrinterForm" class="api-form" data-method="PUT" data-response="editPrinterResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="editPrinterId" name="printerId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editPrinterName" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control" id="editPrinterName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editPrinterDescription" class="form-label">Beschreibung</label>
|
||||||
|
<textarea class="form-control" id="editPrinterDescription" name="description" rows="3" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editPrinterStatus" class="form-label">Status</label>
|
||||||
|
<select class="form-control" id="editPrinterStatus" name="status">
|
||||||
|
<option value="0">Verfügbar (0)</option>
|
||||||
|
<option value="1">Besetzt (1)</option>
|
||||||
|
<option value="2">Wartung (2)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editPrinterIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label>
|
||||||
|
<input type="text" class="form-control" id="editPrinterIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="editPrinterResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="editPrinterForm" class="btn btn-primary">Änderungen speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drucker löschen Modal -->
|
||||||
|
<div class="modal fade" id="deletePrinterModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Drucker löschen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Möchten Sie den Drucker <span id="deletePrinterName"></span> wirklich löschen?</p>
|
||||||
|
<form id="deletePrinterForm" class="api-form" data-method="DELETE" data-response="deletePrinterResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="deletePrinterId" name="printerId">
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="deletePrinterResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="deletePrinterForm" class="btn btn-danger">Löschen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Drucker laden
|
||||||
|
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
||||||
|
|
||||||
|
// Tabelle aktualisieren, wenn Drucker geladen werden
|
||||||
|
const printersResponse = document.getElementById('printersResponse');
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
try {
|
||||||
|
const printers = JSON.parse(printersResponse.textContent);
|
||||||
|
updatePrintersTable(printers);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Parsen der Drucker-Daten:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(printersResponse, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// Edit-Modal vorbereiten
|
||||||
|
document.getElementById('editPrinterModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const printerId = button.getAttribute('data-printer-id');
|
||||||
|
const printerName = button.getAttribute('data-printer-name');
|
||||||
|
const printerDescription = button.getAttribute('data-printer-description');
|
||||||
|
const printerStatus = button.getAttribute('data-printer-status');
|
||||||
|
const printerIpAddress = button.getAttribute('data-printer-ip');
|
||||||
|
|
||||||
|
document.getElementById('editPrinterId').value = printerId;
|
||||||
|
document.getElementById('editPrinterForm').setAttribute('data-url', `/api/printers/${printerId}`);
|
||||||
|
document.getElementById('editPrinterName').value = printerName;
|
||||||
|
document.getElementById('editPrinterDescription').value = printerDescription;
|
||||||
|
document.getElementById('editPrinterStatus').value = printerStatus;
|
||||||
|
document.getElementById('editPrinterIpAddress').value = printerIpAddress || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete-Modal vorbereiten
|
||||||
|
document.getElementById('deletePrinterModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const printerId = button.getAttribute('data-printer-id');
|
||||||
|
const printerName = button.getAttribute('data-printer-name');
|
||||||
|
|
||||||
|
document.getElementById('deletePrinterId').value = printerId;
|
||||||
|
document.getElementById('deletePrinterForm').setAttribute('data-url', `/api/printers/${printerId}`);
|
||||||
|
document.getElementById('deletePrinterName').textContent = printerName;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updatePrintersTable(printers) {
|
||||||
|
const tableBody = document.getElementById('printersTableBody');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
printers.forEach(printer => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
const statusText = {
|
||||||
|
0: 'Verfügbar',
|
||||||
|
1: 'Besetzt',
|
||||||
|
2: 'Wartung'
|
||||||
|
}[printer.status] || 'Unbekannt';
|
||||||
|
|
||||||
|
const statusClass = {
|
||||||
|
0: 'text-success',
|
||||||
|
1: 'text-warning',
|
||||||
|
2: 'text-danger'
|
||||||
|
}[printer.status] || '';
|
||||||
|
|
||||||
|
// Informationen zum aktuellen Job
|
||||||
|
let currentJobInfo = '-';
|
||||||
|
if (printer.latestJob && printer.status === 1) {
|
||||||
|
// Verbleibende Zeit berechnen
|
||||||
|
const remainingTime = printer.latestJob.remainingMinutes || 0;
|
||||||
|
currentJobInfo = `
|
||||||
|
<div class="small">
|
||||||
|
<strong>ID:</strong> ${printer.latestJob.id.substring(0, 8)}...<br>
|
||||||
|
<strong>Dauer:</strong> ${printer.latestJob.durationInMinutes} Min<br>
|
||||||
|
<strong>Verbleibend:</strong> ${remainingTime} Min
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wartende Jobs anzeigen
|
||||||
|
let waitingJobsInfo = '-';
|
||||||
|
if (printer.waitingJobs && printer.waitingJobs.length > 0) {
|
||||||
|
const waitingJobsCount = printer.waitingJobs.length;
|
||||||
|
waitingJobsInfo = `
|
||||||
|
<div class="small">
|
||||||
|
<strong>${waitingJobsCount} Job${waitingJobsCount !== 1 ? 's' : ''} in Warteschlange</strong><br>
|
||||||
|
${printer.waitingJobs.map((job, index) =>
|
||||||
|
`<span>${index + 1}. Job ${job.id.substring(0, 8)}... (${job.durationInMinutes} Min)</span>`
|
||||||
|
).join('<br>')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${printer.id}</td>
|
||||||
|
<td>${printer.name}</td>
|
||||||
|
<td>${printer.description}</td>
|
||||||
|
<td><span class="${statusClass}">${statusText} (${printer.status})</span></td>
|
||||||
|
<td>${printer.ipAddress || '-'}</td>
|
||||||
|
<td>${currentJobInfo}</td>
|
||||||
|
<td>${waitingJobsInfo}</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editPrinterModal"
|
||||||
|
data-printer-id="${printer.id}"
|
||||||
|
data-printer-name="${printer.name}"
|
||||||
|
data-printer-description="${printer.description}"
|
||||||
|
data-printer-status="${printer.status}"
|
||||||
|
data-printer-ip="${printer.ipAddress || ''}">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deletePrinterModal"
|
||||||
|
data-printer-id="${printer.id}"
|
||||||
|
data-printer-name="${printer.name}">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
45
backend/templates/register.html
Normal file
45
backend/templates/register.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Registrieren - MYP API Tester{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Registrieren</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form" data-url="/auth/register" data-method="POST" data-response="registerResponse">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Benutzername</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Passwort</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="displayName" class="form-label">Anzeigename</label>
|
||||||
|
<input type="text" class="form-control" id="displayName" name="displayName">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">E-Mail</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Registrieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p>Bereits registriert? <a href="/login">Anmelden</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<h5>Antwort:</h5>
|
||||||
|
<pre class="api-response" id="registerResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
395
backend/templates/stats.html
Normal file
395
backend/templates/stats.html
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Statistiken - MYP API Tester{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Systemstatistiken</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form mb-3" data-url="/api/stats" data-method="GET" data-response="statsResponse">
|
||||||
|
<button type="submit" class="btn btn-primary">Statistiken aktualisieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="row" id="statsContainer">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Problem-Drucker-Bereich -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0">Drucker mit Verbindungsproblemen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="problemPrintersContainer">
|
||||||
|
<div class="alert alert-info">Keine Verbindungsprobleme festgestellt.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Uptime-Grafik -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-dark text-white">
|
||||||
|
<h5 class="mb-0">Steckdosen-Verfügbarkeit</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form mb-3" data-url="/api/uptime" data-method="GET" data-response="uptimeResponse">
|
||||||
|
<button type="submit" class="btn btn-primary">Uptime-Daten laden</button>
|
||||||
|
</form>
|
||||||
|
<canvas id="uptimeChart" width="100%" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API-Antworten -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Stats API-Antwort:</h6>
|
||||||
|
<pre class="api-response" id="statsResponse"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Uptime API-Antwort:</h6>
|
||||||
|
<pre class="api-response" id="uptimeResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<!-- Chart.js für Diagramme -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let uptimeChart;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Statistiken laden
|
||||||
|
document.querySelector('form[data-url="/api/stats"]').dispatchEvent(new Event('submit'));
|
||||||
|
document.querySelector('form[data-url="/api/uptime"]').dispatchEvent(new Event('submit'));
|
||||||
|
|
||||||
|
// Statistiken aktualisieren, wenn API-Antwort geladen wird
|
||||||
|
const statsResponse = document.getElementById('statsResponse');
|
||||||
|
const statsObserver = new MutationObserver(function(mutations) {
|
||||||
|
try {
|
||||||
|
const stats = JSON.parse(statsResponse.textContent);
|
||||||
|
updateStatsDisplay(stats);
|
||||||
|
updateProblemPrinters(stats);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Parsen der Statistik-Daten:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
statsObserver.observe(statsResponse, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// Uptime-Daten aktualisieren, wenn API-Antwort geladen wird
|
||||||
|
const uptimeResponse = document.getElementById('uptimeResponse');
|
||||||
|
const uptimeObserver = new MutationObserver(function(mutations) {
|
||||||
|
try {
|
||||||
|
const uptime = JSON.parse(uptimeResponse.textContent);
|
||||||
|
updateUptimeChart(uptime);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Parsen der Uptime-Daten:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uptimeObserver.observe(uptimeResponse, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// Periodische Aktualisierung
|
||||||
|
setInterval(function() {
|
||||||
|
document.querySelector('form[data-url="/api/stats"]').dispatchEvent(new Event('submit'));
|
||||||
|
document.querySelector('form[data-url="/api/uptime"]').dispatchEvent(new Event('submit'));
|
||||||
|
}, 60000); // Alle 60 Sekunden aktualisieren
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatsDisplay(stats) {
|
||||||
|
const container = document.getElementById('statsContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Drucker-Statistiken
|
||||||
|
const printerStats = document.createElement('div');
|
||||||
|
printerStats.className = 'col-md-4 mb-3';
|
||||||
|
printerStats.innerHTML = `
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">Drucker</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Gesamt:</span>
|
||||||
|
<span>${stats.printers.total}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Verfügbar:</span>
|
||||||
|
<span>${stats.printers.available}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Auslastung:</span>
|
||||||
|
<span>${Math.round(stats.printers.utilization_rate * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-3 mb-3">
|
||||||
|
<div class="progress-bar" role="progressbar"
|
||||||
|
style="width: ${Math.round(stats.printers.utilization_rate * 100)}%">
|
||||||
|
${Math.round(stats.printers.utilization_rate * 100)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Online:</span>
|
||||||
|
<span>${stats.printers.online}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Offline:</span>
|
||||||
|
<span>${stats.printers.offline}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Verbindungsrate:</span>
|
||||||
|
<span>${Math.round(stats.printers.connectivity_rate * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-3">
|
||||||
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
|
style="width: ${Math.round(stats.printers.connectivity_rate * 100)}%">
|
||||||
|
${Math.round(stats.printers.connectivity_rate * 100)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Job-Statistiken
|
||||||
|
const jobStats = document.createElement('div');
|
||||||
|
jobStats.className = 'col-md-4 mb-3';
|
||||||
|
jobStats.innerHTML = `
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0">Druckaufträge</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Gesamt:</span>
|
||||||
|
<span>${stats.jobs.total}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Aktiv:</span>
|
||||||
|
<span>${stats.jobs.active}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Abgeschlossen:</span>
|
||||||
|
<span>${stats.jobs.completed}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Durchschnittliche Dauer:</span>
|
||||||
|
<span>${stats.jobs.avg_duration} Minuten</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Benutzer- und Uptime-Statistiken
|
||||||
|
const userStats = document.createElement('div');
|
||||||
|
userStats.className = 'col-md-4 mb-3';
|
||||||
|
userStats.innerHTML = `
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0">System</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Benutzer:</span>
|
||||||
|
<span>${stats.users.total}</span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Verbindungsausfälle (7 Tage):</span>
|
||||||
|
<span>${stats.uptime.outages_last_7_days}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Aktuelle Probleme:</span>
|
||||||
|
<span>${stats.uptime.problem_printers.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(printerStats);
|
||||||
|
container.appendChild(jobStats);
|
||||||
|
container.appendChild(userStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProblemPrinters(stats) {
|
||||||
|
const container = document.getElementById('problemPrintersContainer');
|
||||||
|
const problemPrinters = stats.uptime.problem_printers;
|
||||||
|
|
||||||
|
if (problemPrinters.length === 0) {
|
||||||
|
container.innerHTML = '<div class="alert alert-info">Keine Verbindungsprobleme festgestellt.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="table-responsive"><table class="table table-striped">';
|
||||||
|
html += '<thead><tr><th>Drucker</th><th>Status</th><th>Offline seit</th><th>Dauer</th></tr></thead>';
|
||||||
|
html += '<tbody>';
|
||||||
|
|
||||||
|
problemPrinters.forEach(printer => {
|
||||||
|
let offlineSince = 'Unbekannt';
|
||||||
|
let duration = 'Unbekannt';
|
||||||
|
|
||||||
|
if (printer.last_seen) {
|
||||||
|
try {
|
||||||
|
const lastSeen = new Date(printer.last_seen);
|
||||||
|
const now = new Date();
|
||||||
|
const diffSeconds = Math.floor((now - lastSeen) / 1000);
|
||||||
|
const hours = Math.floor(diffSeconds / 3600);
|
||||||
|
const minutes = Math.floor((diffSeconds % 3600) / 60);
|
||||||
|
|
||||||
|
offlineSince = lastSeen.toLocaleString();
|
||||||
|
duration = `${hours}h ${minutes}m`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Berechnen der Offline-Zeit:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<tr>
|
||||||
|
<td>${printer.name}</td>
|
||||||
|
<td><span class="badge bg-danger">Offline</span></td>
|
||||||
|
<td>${offlineSince}</td>
|
||||||
|
<td>${duration}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUptimeChart(uptimeData) {
|
||||||
|
// Wenn keine Daten vorhanden sind, nichts tun
|
||||||
|
if (!uptimeData || !uptimeData.sockets || uptimeData.sockets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daten für das Diagramm vorbereiten
|
||||||
|
const socketNames = [];
|
||||||
|
const datasets = [];
|
||||||
|
const colors = {
|
||||||
|
online: 'rgba(40, 167, 69, 0.7)',
|
||||||
|
offline: 'rgba(220, 53, 69, 0.7)',
|
||||||
|
unknown: 'rgba(108, 117, 125, 0.7)'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zeitraum für das Diagramm (letzten 7 Tage)
|
||||||
|
const endDate = new Date();
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - 7);
|
||||||
|
|
||||||
|
// Für jede Steckdose
|
||||||
|
uptimeData.sockets.forEach(socket => {
|
||||||
|
socketNames.push(socket.name);
|
||||||
|
|
||||||
|
// Sortiere Ereignisse nach Zeitstempel
|
||||||
|
if (socket.events) {
|
||||||
|
socket.events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||||
|
|
||||||
|
// Erstelle einen Datensatz für diese Steckdose
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
// Füge Ereignisse zum Datensatz hinzu
|
||||||
|
socket.events.forEach(event => {
|
||||||
|
data.push({
|
||||||
|
x: new Date(event.timestamp),
|
||||||
|
y: event.status === 'online' ? 1 : 0,
|
||||||
|
status: event.status,
|
||||||
|
duration: event.duration_seconds ?
|
||||||
|
formatDuration(event.duration_seconds) : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Füge aktuellen Status hinzu
|
||||||
|
if (socket.current_status) {
|
||||||
|
data.push({
|
||||||
|
x: new Date(),
|
||||||
|
y: socket.current_status.connection_status === 'online' ? 1 : 0,
|
||||||
|
status: socket.current_status.connection_status,
|
||||||
|
duration: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
datasets.push({
|
||||||
|
label: socket.name,
|
||||||
|
data: data,
|
||||||
|
stepped: true,
|
||||||
|
borderColor: colors[socket.current_status?.connection_status || 'unknown'],
|
||||||
|
backgroundColor: colors[socket.current_status?.connection_status || 'unknown'],
|
||||||
|
fill: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chart.js Konfiguration
|
||||||
|
const ctx = document.getElementById('uptimeChart').getContext('2d');
|
||||||
|
|
||||||
|
// Wenn Chart bereits existiert, zerstöre ihn
|
||||||
|
if (uptimeChart) {
|
||||||
|
uptimeChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle neuen Chart
|
||||||
|
uptimeChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
const point = context.raw;
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
label += ': ' + (point.status === 'online' ? 'Online' : 'Offline');
|
||||||
|
if (point.duration) {
|
||||||
|
label += ' (Dauer: ' + point.duration + ')';
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'day'
|
||||||
|
},
|
||||||
|
min: startDate,
|
||||||
|
max: endDate
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: -0.1,
|
||||||
|
max: 1.1,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value === 0 ? 'Offline' : value === 1 ? 'Online' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
238
backend/templates/users.html
Normal file
238
backend/templates/users.html
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Benutzer - MYP API Tester{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h4 class="mb-0">Benutzer verwalten</h4>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newUserForm">
|
||||||
|
Neuen Benutzer hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="newUserForm">
|
||||||
|
<div class="card-body border-bottom">
|
||||||
|
<form class="api-form" data-url="/auth/register" data-method="POST" data-response="createUserResponse" data-reload="true">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userName" class="form-label">Benutzername</label>
|
||||||
|
<input type="text" class="form-control" id="userName" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userPassword" class="form-label">Passwort</label>
|
||||||
|
<input type="password" class="form-control" id="userPassword" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userDisplayName" class="form-label">Anzeigename</label>
|
||||||
|
<input type="text" class="form-control" id="userDisplayName" name="displayName">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userEmail" class="form-label">E-Mail</label>
|
||||||
|
<input type="email" class="form-control" id="userEmail" name="email">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">Benutzer erstellen</button>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="createUserResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="api-form mb-3" data-url="/api/users" data-method="GET" data-response="usersResponse">
|
||||||
|
<button type="submit" class="btn btn-primary">Benutzer aktualisieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Benutzername</th>
|
||||||
|
<th>Anzeigename</th>
|
||||||
|
<th>E-Mail</th>
|
||||||
|
<th>Rolle</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="usersTableBody">
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h6>API-Antwort:</h6>
|
||||||
|
<pre class="api-response" id="usersResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Benutzer bearbeiten Modal -->
|
||||||
|
<div class="modal fade" id="editUserModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Benutzer bearbeiten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editUserForm" class="api-form" data-method="PUT" data-response="editUserResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="editUserId" name="userId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editUserName" class="form-label">Benutzername</label>
|
||||||
|
<input type="text" class="form-control" id="editUserName" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editUserDisplayName" class="form-label">Anzeigename</label>
|
||||||
|
<input type="text" class="form-control" id="editUserDisplayName" name="displayName">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editUserEmail" class="form-label">E-Mail</label>
|
||||||
|
<input type="email" class="form-control" id="editUserEmail" name="email">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editUserRole" class="form-label">Rolle</label>
|
||||||
|
<select class="form-control" id="editUserRole" name="role">
|
||||||
|
<option value="user">Benutzer</option>
|
||||||
|
<option value="admin">Administrator</option>
|
||||||
|
<option value="guest">Gast</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="editUserResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="editUserForm" class="btn btn-primary">Änderungen speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Benutzer löschen Modal -->
|
||||||
|
<div class="modal fade" id="deleteUserModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Benutzer löschen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Möchten Sie den Benutzer <span id="deleteUserName"></span> wirklich löschen?</p>
|
||||||
|
<form id="deleteUserForm" class="api-form" data-method="DELETE" data-response="deleteUserResponse" data-reload="true">
|
||||||
|
<input type="hidden" id="deleteUserId" name="userId">
|
||||||
|
</form>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Antwort:</h6>
|
||||||
|
<pre class="api-response" id="deleteUserResponse"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" form="deleteUserForm" class="btn btn-danger">Löschen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Benutzer laden
|
||||||
|
document.querySelector('form[data-url="/api/users"]').dispatchEvent(new Event('submit'));
|
||||||
|
|
||||||
|
// Tabelle aktualisieren, wenn Benutzer geladen werden
|
||||||
|
const usersResponse = document.getElementById('usersResponse');
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
try {
|
||||||
|
const users = JSON.parse(usersResponse.textContent);
|
||||||
|
updateUsersTable(users);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Parsen der Benutzer-Daten:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(usersResponse, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// Edit-Modal vorbereiten
|
||||||
|
document.getElementById('editUserModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const userId = button.getAttribute('data-user-id');
|
||||||
|
const userName = button.getAttribute('data-user-name');
|
||||||
|
const userDisplayName = button.getAttribute('data-user-displayname');
|
||||||
|
const userEmail = button.getAttribute('data-user-email');
|
||||||
|
const userRole = button.getAttribute('data-user-role');
|
||||||
|
|
||||||
|
document.getElementById('editUserId').value = userId;
|
||||||
|
document.getElementById('editUserForm').setAttribute('data-url', `/api/users/${userId}`);
|
||||||
|
document.getElementById('editUserName').value = userName;
|
||||||
|
document.getElementById('editUserDisplayName').value = userDisplayName || '';
|
||||||
|
document.getElementById('editUserEmail').value = userEmail || '';
|
||||||
|
document.getElementById('editUserRole').value = userRole;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete-Modal vorbereiten
|
||||||
|
document.getElementById('deleteUserModal').addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const userId = button.getAttribute('data-user-id');
|
||||||
|
const userName = button.getAttribute('data-user-name');
|
||||||
|
|
||||||
|
document.getElementById('deleteUserId').value = userId;
|
||||||
|
document.getElementById('deleteUserForm').setAttribute('data-url', `/api/users/${userId}`);
|
||||||
|
document.getElementById('deleteUserName').textContent = userName;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateUsersTable(users) {
|
||||||
|
const tableBody = document.getElementById('usersTableBody');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
users.forEach(user => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
const roleClass = {
|
||||||
|
'admin': 'text-danger',
|
||||||
|
'user': 'text-primary',
|
||||||
|
'guest': 'text-secondary'
|
||||||
|
}[user.role] || '';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${user.id}</td>
|
||||||
|
<td>${user.username}</td>
|
||||||
|
<td>${user.displayName || user.username}</td>
|
||||||
|
<td>${user.email || '-'}</td>
|
||||||
|
<td><span class="${roleClass}">${user.role}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editUserModal"
|
||||||
|
data-user-id="${user.id}"
|
||||||
|
data-user-name="${user.username}"
|
||||||
|
data-user-displayname="${user.displayName || ''}"
|
||||||
|
data-user-email="${user.email || ''}"
|
||||||
|
data-user-role="${user.role}">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteUserModal"
|
||||||
|
data-user-id="${user.id}"
|
||||||
|
data-user-name="${user.username}">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
230
configure-oauth.sh
Executable file
230
configure-oauth.sh
Executable file
@ -0,0 +1,230 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Skript zum Konfigurieren von OAuth-Variablen für MYP-Projekt
|
||||||
|
# Fügt die Variablen in die Umgebungsdatei ein und aktualisiert den laufenden Container
|
||||||
|
# Erstellt von Claude für das MYP-Projekt basierend auf bestehender Konfiguration
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# sudo ./configure-oauth.sh
|
||||||
|
#
|
||||||
|
# Dieses Skript konfiguriert die für die GitHub OAuth-Integration notwendigen
|
||||||
|
# Umgebungsvariablen und speichert sie in /srv/myp-env/github.env.
|
||||||
|
# Außerdem aktualisiert es die Variablen im laufenden Docker-Container, falls vorhanden.
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktion zur Ausgabe mit Zeitstempel
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
success_log() {
|
||||||
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ERFOLG:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Definiere Variablen
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
ENV_FILE="/srv/myp-env/github.env"
|
||||||
|
CONTAINER_NAME="myp-rp"
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
|
||||||
|
# Prüfe, ob der Benutzer Root-Berechtigungen hat
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
error_log "Dieses Skript muss mit Root-Rechten ausgeführt werden (sudo)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stelle sicher, dass das Verzeichnis existiert
|
||||||
|
if ! mkdir -p /srv/myp-env 2>/dev/null; then
|
||||||
|
error_log "Konnte Verzeichnis /srv/myp-env nicht erstellen. Überprüfen Sie Ihre Berechtigungen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bestimme den Hostnamen für OAuth
|
||||||
|
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
||||||
|
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||||
|
DEFAULT_CALLBACK_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
|
||||||
|
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
else
|
||||||
|
FRONTEND_HOSTNAME="$HOSTNAME"
|
||||||
|
DEFAULT_CALLBACK_URL="http://$HOSTNAME:3000/auth/login/callback"
|
||||||
|
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lade vorhandene Werte, falls die Datei existiert
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
log "Lade vorhandene Werte aus $ENV_FILE..."
|
||||||
|
source "$ENV_FILE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Frage die Werte vom Benutzer ab
|
||||||
|
header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}===== $1 =====${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
header "OAuth-Konfiguration für MYP-Projekt"
|
||||||
|
echo "Dieses Skript konfiguriert die OAuth-Integration für das MYP Frontend."
|
||||||
|
echo "Die Konfiguration wird in $ENV_FILE gespeichert und im Container aktualisiert."
|
||||||
|
echo ""
|
||||||
|
echo "Bitte geben Sie die folgenden Werte ein:"
|
||||||
|
echo "Drücken Sie einfach Enter, um die Standardwerte zu verwenden."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Bestimme GitHub-Server-Typ
|
||||||
|
echo -e "${YELLOW}GitHub-Typ auswählen${NC}"
|
||||||
|
echo -e "1) GitHub Enterprise (Mercedes-Benz git.i.mercedes-benz.com)"
|
||||||
|
echo -e "2) Öffentliches GitHub (github.com)"
|
||||||
|
read -p "Auswahl (1/2) [1]: " github_type_choice
|
||||||
|
github_type_choice=${github_type_choice:-1}
|
||||||
|
|
||||||
|
if [[ "$github_type_choice" == "1" ]]; then
|
||||||
|
GITHUB_ENTERPRISE=true
|
||||||
|
GITHUB_DOMAIN="https://git.i.mercedes-benz.com"
|
||||||
|
echo -e "\nVerwende ${GREEN}GitHub Enterprise${NC} ($GITHUB_DOMAIN)"
|
||||||
|
else
|
||||||
|
GITHUB_ENTERPRISE=false
|
||||||
|
GITHUB_DOMAIN="https://github.com"
|
||||||
|
echo -e "\nVerwende ${GREEN}GitHub.com${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# OAuth Callback URL
|
||||||
|
echo -e "\n${YELLOW}OAuth Callback URL${NC}"
|
||||||
|
echo -e "URL für die Weiterleitung nach der GitHub-Authentifizierung"
|
||||||
|
echo -e "Diese muss exakt mit der in der GitHub OAuth App konfigurierten URL übereinstimmen."
|
||||||
|
read -p "NEXT_PUBLIC_OAUTH_CALLBACK_URL [$DEFAULT_CALLBACK_URL]: " user_oauth_callback
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${user_oauth_callback:-$DEFAULT_CALLBACK_URL}
|
||||||
|
OAUTH_CALLBACK_URL=$NEXT_PUBLIC_OAUTH_CALLBACK_URL
|
||||||
|
|
||||||
|
# GitHub OAuth Anmeldedaten
|
||||||
|
echo -e "\n${YELLOW}GitHub OAuth Client ID${NC}"
|
||||||
|
echo -e "Aus der GitHub OAuth App-Konfiguration"
|
||||||
|
read -p "AUTH_GITHUB_CLIENT_ID: " user_client_id
|
||||||
|
AUTH_GITHUB_CLIENT_ID=${user_client_id:-$AUTH_GITHUB_CLIENT_ID}
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}GitHub OAuth Client Secret${NC}"
|
||||||
|
echo -e "Aus der GitHub OAuth App-Konfiguration"
|
||||||
|
read -p "AUTH_GITHUB_CLIENT_SECRET: " user_client_secret
|
||||||
|
AUTH_GITHUB_CLIENT_SECRET=${user_client_secret:-$AUTH_GITHUB_CLIENT_SECRET}
|
||||||
|
|
||||||
|
# Prüfe, ob alle erforderlichen Werte gesetzt sind
|
||||||
|
if [ -z "$AUTH_GITHUB_CLIENT_ID" ] || [ -z "$AUTH_GITHUB_CLIENT_SECRET" ]; then
|
||||||
|
error_log "Bitte geben Sie gültige Werte für AUTH_GITHUB_CLIENT_ID und AUTH_GITHUB_CLIENT_SECRET an."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktualisiere die Umgebungsdatei
|
||||||
|
log "Aktualisiere Umgebungsvariablen in $ENV_FILE..."
|
||||||
|
|
||||||
|
# Sichere die alte Datei, falls sie existiert
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
cp "$ENV_FILE" "${ENV_FILE}.bak"
|
||||||
|
log "Sicherungskopie erstellt: ${ENV_FILE}.bak"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle eine neue temporäre Datei
|
||||||
|
TMP_ENV_FILE=$(mktemp)
|
||||||
|
|
||||||
|
# Wenn die alte Datei existiert, übernehme alle Zeilen außer den zu ändernden
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
grep -v "NEXT_PUBLIC_OAUTH_CALLBACK_URL\|OAUTH_CALLBACK_URL\|AUTH_GITHUB_CLIENT_ID\|AUTH_GITHUB_CLIENT_SECRET\|OAUTH_CLIENT_ID\|OAUTH_CLIENT_SECRET\|GITHUB_ENTERPRISE\|GITHUB_DOMAIN" "$ENV_FILE" > "$TMP_ENV_FILE" || true
|
||||||
|
else
|
||||||
|
touch "$TMP_ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Füge die neuen Werte hinzu
|
||||||
|
cat >> "$TMP_ENV_FILE" << EOL
|
||||||
|
# OAuth-Konfiguration (Aktualisiert: $(date))
|
||||||
|
# Konfiguriert mit configure-oauth.sh
|
||||||
|
|
||||||
|
# OAuth Callback URL
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${NEXT_PUBLIC_OAUTH_CALLBACK_URL}
|
||||||
|
OAUTH_CALLBACK_URL=${OAUTH_CALLBACK_URL}
|
||||||
|
|
||||||
|
# GitHub OAuth Credentials
|
||||||
|
AUTH_GITHUB_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID}
|
||||||
|
AUTH_GITHUB_CLIENT_SECRET=${AUTH_GITHUB_CLIENT_SECRET}
|
||||||
|
|
||||||
|
# Kompatibilitäts-Variablen (werden für ältere Versionen benötigt)
|
||||||
|
OAUTH_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID}
|
||||||
|
OAUTH_CLIENT_SECRET=${AUTH_GITHUB_CLIENT_SECRET}
|
||||||
|
|
||||||
|
# GitHub Server-Konfiguration
|
||||||
|
GITHUB_ENTERPRISE=${GITHUB_ENTERPRISE}
|
||||||
|
GITHUB_DOMAIN=${GITHUB_DOMAIN}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Verschiebe die temporäre Datei an den Zielort
|
||||||
|
mv "$TMP_ENV_FILE" "$ENV_FILE"
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
|
success_log "Umgebungsvariablen erfolgreich aktualisiert."
|
||||||
|
|
||||||
|
# Prüfe, ob der Docker-Container läuft
|
||||||
|
if docker ps -q -f name=$CONTAINER_NAME | grep -q .; then
|
||||||
|
log "Container $CONTAINER_NAME läuft. Aktualisiere Umgebungsvariablen..."
|
||||||
|
|
||||||
|
# Kopiere die Umgebungsdatei in den Container
|
||||||
|
docker cp "$ENV_FILE" "$CONTAINER_NAME:/app/.env"
|
||||||
|
|
||||||
|
# Setze die Umgebungsvariablen für den laufenden Container
|
||||||
|
docker exec "$CONTAINER_NAME" /bin/sh -c "
|
||||||
|
export NEXT_PUBLIC_OAUTH_CALLBACK_URL=\"$NEXT_PUBLIC_OAUTH_CALLBACK_URL\"
|
||||||
|
export OAUTH_CALLBACK_URL=\"$OAUTH_CALLBACK_URL\"
|
||||||
|
export AUTH_GITHUB_CLIENT_ID=\"$AUTH_GITHUB_CLIENT_ID\"
|
||||||
|
export AUTH_GITHUB_CLIENT_SECRET=\"$AUTH_GITHUB_CLIENT_SECRET\"
|
||||||
|
export OAUTH_CLIENT_ID=\"$AUTH_GITHUB_CLIENT_ID\"
|
||||||
|
export OAUTH_CLIENT_SECRET=\"$AUTH_GITHUB_CLIENT_SECRET\"
|
||||||
|
export GITHUB_ENTERPRISE=\"$GITHUB_ENTERPRISE\"
|
||||||
|
export GITHUB_DOMAIN=\"$GITHUB_DOMAIN\"
|
||||||
|
echo \"Umgebungsvariablen gesetzt:\"
|
||||||
|
env | grep -E 'OAUTH|AUTH|GITHUB'
|
||||||
|
"
|
||||||
|
|
||||||
|
# Neustart des Containers empfehlen
|
||||||
|
log "${YELLOW}Es wird empfohlen, den Container neu zu starten, damit alle Änderungen wirksam werden:${NC}"
|
||||||
|
echo " docker restart $CONTAINER_NAME"
|
||||||
|
|
||||||
|
# Frage, ob der Container neu gestartet werden soll
|
||||||
|
read -p "Container jetzt neu starten? (j/n): " restart_choice
|
||||||
|
if [[ "$restart_choice" == "j" ]]; then
|
||||||
|
log "Starte Container neu..."
|
||||||
|
docker restart "$CONTAINER_NAME"
|
||||||
|
success_log "Container neu gestartet. Änderungen sollten jetzt wirksam sein."
|
||||||
|
else
|
||||||
|
log "${YELLOW}Container nicht neu gestartet. Bitte manuell neu starten, wenn nötig.${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "${YELLOW}Container $CONTAINER_NAME läuft nicht. Die Änderungen werden beim nächsten Start wirksam.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Zeige die konfigurierten Werte an
|
||||||
|
echo ""
|
||||||
|
success_log "OAuth-Konfiguration abgeschlossen!"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Konfigurierte Werte:${NC}"
|
||||||
|
echo -e "GitHub Server: ${YELLOW}$( [ "$GITHUB_ENTERPRISE" = "true" ] && echo "Enterprise ($GITHUB_DOMAIN)" || echo "GitHub.com" )${NC}"
|
||||||
|
echo -e "NEXT_PUBLIC_OAUTH_CALLBACK_URL: ${YELLOW}$NEXT_PUBLIC_OAUTH_CALLBACK_URL${NC}"
|
||||||
|
echo -e "OAUTH_CALLBACK_URL: ${YELLOW}$OAUTH_CALLBACK_URL${NC}"
|
||||||
|
echo -e "AUTH_GITHUB_CLIENT_ID: ${YELLOW}$AUTH_GITHUB_CLIENT_ID${NC}"
|
||||||
|
echo -e "AUTH_GITHUB_CLIENT_SECRET: ${YELLOW}${AUTH_GITHUB_CLIENT_SECRET:0:5}...${NC} (aus Sicherheitsgründen gekürzt)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log "Konfiguration wurde in $ENV_FILE gespeichert."
|
||||||
|
if docker ps -q -f name=$CONTAINER_NAME | grep -q .; then
|
||||||
|
log "Container ${YELLOW}$CONTAINER_NAME${NC} wurde aktualisiert."
|
||||||
|
log "Starten Sie den Container neu mit: ${YELLOW}docker restart $CONTAINER_NAME${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
log "Sie können jetzt das Frontend mit Ihrer GitHub OAuth-App-Konfiguration verwenden."
|
6
debug.sh
Executable file
6
debug.sh
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
rm log.txt
|
||||||
|
docker logs myp-frontend >> log.txt
|
||||||
|
git add *
|
||||||
|
git commit -m "logs"
|
||||||
|
git push
|
0
docs/.gitkeep
Normal file → Executable file
0
docs/.gitkeep
Normal file → Executable file
0
docs/Aktueller Stand.md
Normal file → Executable file
0
docs/Aktueller Stand.md
Normal file → Executable file
10
docs/Dokumentation_IHK.md
Normal file → Executable file
10
docs/Dokumentation_IHK.md
Normal file → Executable file
@ -8,6 +8,16 @@ Notizen:
|
|||||||
- Da Till digitale Vernetzung hat macht er Backend, weil die Schnittstelle der Vernetzung zum cyberphysischen System dort lag
|
- Da Till digitale Vernetzung hat macht er Backend, weil die Schnittstelle der Vernetzung zum cyberphysischen System dort lag
|
||||||
- für die Dokumentation: Daten (Datums) müssen stimmen!
|
- für die Dokumentation: Daten (Datums) müssen stimmen!
|
||||||
|
|
||||||
|
python schnittstelle funktionierte nicht
|
||||||
|
nach etlichem rumprobieren festgestellt: geht nicht so einfach
|
||||||
|
wireshark mitschnitt gemacht → auffällig: immer die selben responses bei verschlüsselter verbindung
|
||||||
|
ohne erfolg beim simulieren einzelner anfragen
|
||||||
|
dann: geistesblitz: anfragensequenz muss es sein!
|
||||||
|
hat funktioniert → es hat klick gemacht!! .
|
||||||
|
verbindung verschlüsselt und mit temporärem cookie
|
||||||
|
→ proprietäre Verschlüsselung
|
||||||
|
wie wird die verbindung ausgehandelt?
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
11.09 : Teile bestellt im internen Technikshop
|
11.09 : Teile bestellt im internen Technikshop
|
||||||
|
0
docs/Infrastruktur.png
Normal file → Executable file
0
docs/Infrastruktur.png
Normal file → Executable file
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 3.3 MiB |
0
docs/Infrastruktur.tldr
Normal file → Executable file
0
docs/Infrastruktur.tldr
Normal file → Executable file
0
docs/MYP.dbml
Normal file → Executable file
0
docs/MYP.dbml
Normal file → Executable file
0
docs/MYP.png
Normal file → Executable file
0
docs/MYP.png
Normal file → Executable file
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
0
docs/MYP.sql
Normal file → Executable file
0
docs/MYP.sql
Normal file → Executable file
80
docs/README.md
Normal file
80
docs/README.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# MYP OAuth Konfigurationsanleitung
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt, wie die OAuth-Konfiguration für das MYP-Projekt eingerichtet wird.
|
||||||
|
|
||||||
|
## Überblick
|
||||||
|
|
||||||
|
Das MYP Frontend verwendet GitHub OAuth zur Authentifizierung. Die Konfiguration erfolgt über
|
||||||
|
Umgebungsvariablen, die in der Datei `/srv/myp-env/github.env` gespeichert werden.
|
||||||
|
|
||||||
|
## Konfiguration mit configure-oauth.sh
|
||||||
|
|
||||||
|
Wir haben ein Skript erstellt, um die OAuth-Konfiguration zu vereinfachen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./configure-oauth.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Skript führt folgende Aktionen aus:
|
||||||
|
|
||||||
|
1. Erfasst die benötigten OAuth-Konfigurationsinformationen interaktiv
|
||||||
|
2. Speichert diese in `/srv/myp-env/github.env`
|
||||||
|
3. Aktualisiert einen laufenden Docker-Container (falls vorhanden)
|
||||||
|
4. Bietet die Option, den Container neu zu starten
|
||||||
|
|
||||||
|
## Benötigte Informationen
|
||||||
|
|
||||||
|
Für die Konfiguration werden folgende Informationen benötigt:
|
||||||
|
|
||||||
|
1. **GitHub-Typ**: GitHub Enterprise (git.i.mercedes-benz.com) oder GitHub.com
|
||||||
|
2. **OAuth Callback URL**: Die URL, zu der GitHub nach der Authentifizierung zurückleitet
|
||||||
|
- Standard für Unternehmensumgebung: `http://m040tbaraspi001.de040.corpintra.net/auth/login/callback`
|
||||||
|
- Standard für lokale Entwicklung: `http://localhost:3000/auth/login/callback`
|
||||||
|
3. **GitHub OAuth Client ID**: Von der GitHub OAuth App-Konfiguration
|
||||||
|
4. **GitHub OAuth Client Secret**: Von der GitHub OAuth App-Konfiguration
|
||||||
|
|
||||||
|
## Konfiguration der GitHub OAuth App
|
||||||
|
|
||||||
|
1. Navigieren Sie zu Ihren GitHub-Einstellungen (Organisationseinstellungen für Enterprise)
|
||||||
|
2. Wählen Sie "OAuth Apps" oder "Developer Settings" > "OAuth Apps"
|
||||||
|
3. Erstellen Sie eine neue OAuth App mit:
|
||||||
|
- **Name**: MYP (Manage Your Printer)
|
||||||
|
- **Homepage URL**: `http://m040tbaraspi001.de040.corpintra.net` (oder Ihre lokale URL)
|
||||||
|
- **Authorization callback URL**: Exakt die URL, die Sie auch im Skript konfigurieren
|
||||||
|
- **Description**: Optional
|
||||||
|
|
||||||
|
## Umgebungsvariablen
|
||||||
|
|
||||||
|
Die folgenden Umgebungsvariablen werden vom Skript konfiguriert:
|
||||||
|
|
||||||
|
```
|
||||||
|
# OAuth Callback URL
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=http://m040tbaraspi001.de040.corpintra.net/auth/login/callback
|
||||||
|
OAUTH_CALLBACK_URL=http://m040tbaraspi001.de040.corpintra.net/auth/login/callback
|
||||||
|
|
||||||
|
# GitHub OAuth Credentials
|
||||||
|
AUTH_GITHUB_CLIENT_ID=your_client_id
|
||||||
|
AUTH_GITHUB_CLIENT_SECRET=your_client_secret
|
||||||
|
|
||||||
|
# Kompatibilitäts-Variablen
|
||||||
|
OAUTH_CLIENT_ID=your_client_id
|
||||||
|
OAUTH_CLIENT_SECRET=your_client_secret
|
||||||
|
|
||||||
|
# GitHub Server-Konfiguration
|
||||||
|
GITHUB_ENTERPRISE=true
|
||||||
|
GITHUB_DOMAIN=https://git.i.mercedes-benz.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment mit OAuth-Konfiguration
|
||||||
|
|
||||||
|
Die Deployment-Skripte `raspi-frontend-deploy.sh` sind so konfiguriert, dass sie die OAuth-Konfiguration aus
|
||||||
|
`/srv/myp-env/github.env` laden und dem Container zur Verfügung stellen.
|
||||||
|
|
||||||
|
## Fehlerbehebung
|
||||||
|
|
||||||
|
Falls die OAuth-Authentifizierung nicht funktioniert:
|
||||||
|
|
||||||
|
1. Prüfen Sie, ob die Callback-URL exakt mit der in GitHub konfigurierten URL übereinstimmt
|
||||||
|
2. Stellen Sie sicher, dass der Docker-Container die Umgebungsvariablen korrekt übernimmt
|
||||||
|
3. Überprüfen Sie die Netzwerkerreichbarkeit zwischen dem Frontend und dem GitHub-Server
|
||||||
|
4. Prüfen Sie die Frontend-Logs auf OAuth-bezogene Fehlermeldungen
|
90
fehler
Normal file
90
fehler
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
root@m040tbaraspi001:/home/user/Projektarbeit-MYP# ./raspi-frontend-deploy.sh
|
||||||
|
[2025-04-01 15:38:07] Datenbankverzeichnis existiert bereits: /srv/MYP-DB
|
||||||
|
|
||||||
|
===== Backend-URL Konfiguration =====
|
||||||
|
|
||||||
|
[2025-04-01 15:38:07] Standard-Backend-URL: http://192.168.0.105:5000
|
||||||
|
Möchten Sie eine andere Backend-URL verwenden? (j/n): n
|
||||||
|
[2025-04-01 15:38:08] Verwende Standard-Backend-URL: http://192.168.0.105:5000
|
||||||
|
|
||||||
|
===== MYP Frontend Deployment =====
|
||||||
|
|
||||||
|
Bitte wählen Sie eine Option:
|
||||||
|
|
||||||
|
1) Alles automatisch (Build, Deploy, Starten)
|
||||||
|
2) Docker-Image bauen
|
||||||
|
3) Docker-Image speichern
|
||||||
|
4) Docker-Image laden
|
||||||
|
5) Container mit Docker Compose starten
|
||||||
|
6) Container direkt mit Docker Run starten
|
||||||
|
7) Anwendung ohne Docker starten
|
||||||
|
8) Nur Backend-URL konfigurieren
|
||||||
|
9) Beenden
|
||||||
|
|
||||||
|
Ihre Wahl (1-9): 2
|
||||||
|
|
||||||
|
===== Docker-Image bauen =====
|
||||||
|
|
||||||
|
[2025-04-01 15:38:13] Baue Docker-Image: myp-rp:latest
|
||||||
|
[2025-04-01 15:38:13] Baue Docker-Image... (Dies kann auf einem Raspberry Pi mehrere Minuten dauern)
|
||||||
|
[+] Building 74.4s (14/14) FINISHED docker:default
|
||||||
|
=> [internal] load build definition from Dockerfile 0.0s
|
||||||
|
=> => transferring dockerfile: 697B 0.0s
|
||||||
|
=> [internal] load metadata for docker.io/library/node:20-bookworm-slim 1.1s
|
||||||
|
=> [internal] load .dockerignore 0.0s
|
||||||
|
=> => transferring context: 402B 0.0s
|
||||||
|
=> [internal] load build context 0.0s
|
||||||
|
=> => transferring context: 19.02kB 0.0s
|
||||||
|
=> [ 1/10] FROM docker.io/library/node:20-bookworm-slim@sha256:d6e4ec9eaf2390129b5d23904d07ae03ef744818386bcab3fc45fe63405b5eb2 0.0s
|
||||||
|
=> CACHED [ 2/10] RUN mkdir -p /usr/src/app 0.0s
|
||||||
|
=> CACHED [ 3/10] WORKDIR /usr/src/app 0.0s
|
||||||
|
=> CACHED [ 4/10] COPY package.json /usr/src/app 0.0s
|
||||||
|
=> CACHED [ 5/10] COPY pnpm-lock.yaml /usr/src/app 0.0s
|
||||||
|
=> CACHED [ 6/10] RUN corepack enable pnpm 0.0s
|
||||||
|
=> CACHED [ 7/10] RUN pnpm install 0.0s
|
||||||
|
=> [ 8/10] COPY . /usr/src/app 0.3s
|
||||||
|
=> [ 9/10] RUN pnpm run db 5.9s
|
||||||
|
=> ERROR [10/10] RUN pnpm run build 66.7s
|
||||||
|
------
|
||||||
|
> [10/10] RUN pnpm run build:
|
||||||
|
0.917
|
||||||
|
0.917 > myp-rp@1.0.0 build /usr/src/app
|
||||||
|
0.917 > node update-package.js && next build
|
||||||
|
0.917
|
||||||
|
0.968 ℹ️ OAuth-Konfiguration ist bereits aktuell.
|
||||||
|
0.969 ℹ️ OAuth-Callback-Route ist bereits aktuell.
|
||||||
|
0.970 ℹ️ package.json ist bereits aktualisiert.
|
||||||
|
0.970 ✅ OAuth-Konfiguration wurde erfolgreich vorbereitet.
|
||||||
|
1.800 ▲ Next.js 14.2.3
|
||||||
|
1.801 - Environments: .env.local, .env
|
||||||
|
1.801
|
||||||
|
1.821 Creating an optimized production build ...
|
||||||
|
43.85 Browserslist: caniuse-lite is outdated. Please run:
|
||||||
|
43.85 npx update-browserslist-db@latest
|
||||||
|
43.85 Why you should do it regularly: https://github.com/browserslist/update-db#readme
|
||||||
|
50.09 ✓ Compiled successfully
|
||||||
|
50.09 Linting and checking validity of types ...
|
||||||
|
64.70 Failed to compile.
|
||||||
|
64.70
|
||||||
|
64.70 ./src/app/auth/login/route.ts:14:3
|
||||||
|
64.70 Type error: Object literal may only specify known properties, and 'redirectURI' does not exist in type '{ scopes?: string[] | undefined; }'.
|
||||||
|
64.70
|
||||||
|
64.70 12 | const url = await github.createAuthorizationURL(state, {
|
||||||
|
64.70 13 | scopes: ["user"],
|
||||||
|
64.70 > 14 | redirectURI: OAUTH_CALLBACK_URL,
|
||||||
|
64.70 | ^
|
||||||
|
64.70 15 | });
|
||||||
|
64.70 16 | const ONE_HOUR = 60 * 60;
|
||||||
|
64.70 17 |
|
||||||
|
64.75 ELIFECYCLE Command failed with exit code 1.
|
||||||
|
------
|
||||||
|
Dockerfile:29
|
||||||
|
--------------------
|
||||||
|
27 |
|
||||||
|
28 | # Build the application
|
||||||
|
29 | >>> RUN pnpm run build
|
||||||
|
30 |
|
||||||
|
31 | EXPOSE 3000
|
||||||
|
--------------------
|
||||||
|
ERROR: failed to solve: process "/bin/sh -c pnpm run build" did not complete successfully: exit code: 1
|
||||||
|
[2025-04-01 15:39:28] FEHLER: Fehler beim Bauen des Docker-Images.
|
753
get-docker.sh
Executable file
753
get-docker.sh
Executable file
@ -0,0 +1,753 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
# Docker Engine for Linux installation script.
|
||||||
|
#
|
||||||
|
# This script is intended as a convenient way to configure docker's package
|
||||||
|
# repositories and to install Docker Engine, This script is not recommended
|
||||||
|
# for production environments. Before running this script, make yourself familiar
|
||||||
|
# with potential risks and limitations, and refer to the installation manual
|
||||||
|
# at https://docs.docker.com/engine/install/ for alternative installation methods.
|
||||||
|
#
|
||||||
|
# The script:
|
||||||
|
#
|
||||||
|
# - Requires `root` or `sudo` privileges to run.
|
||||||
|
# - Attempts to detect your Linux distribution and version and configure your
|
||||||
|
# package management system for you.
|
||||||
|
# - Doesn't allow you to customize most installation parameters.
|
||||||
|
# - Installs dependencies and recommendations without asking for confirmation.
|
||||||
|
# - Installs the latest stable release (by default) of Docker CLI, Docker Engine,
|
||||||
|
# Docker Buildx, Docker Compose, containerd, and runc. When using this script
|
||||||
|
# to provision a machine, this may result in unexpected major version upgrades
|
||||||
|
# of these packages. Always test upgrades in a test environment before
|
||||||
|
# deploying to your production systems.
|
||||||
|
# - Isn't designed to upgrade an existing Docker installation. When using the
|
||||||
|
# script to update an existing installation, dependencies may not be updated
|
||||||
|
# to the expected version, resulting in outdated versions.
|
||||||
|
#
|
||||||
|
# Source code is available at https://github.com/docker/docker-install/
|
||||||
|
#
|
||||||
|
# Usage
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# To install the latest stable versions of Docker CLI, Docker Engine, and their
|
||||||
|
# dependencies:
|
||||||
|
#
|
||||||
|
# 1. download the script
|
||||||
|
#
|
||||||
|
# $ curl -fsSL https://get.docker.com -o install-docker.sh
|
||||||
|
#
|
||||||
|
# 2. verify the script's content
|
||||||
|
#
|
||||||
|
# $ cat install-docker.sh
|
||||||
|
#
|
||||||
|
# 3. run the script with --dry-run to verify the steps it executes
|
||||||
|
#
|
||||||
|
# $ sh install-docker.sh --dry-run
|
||||||
|
#
|
||||||
|
# 4. run the script either as root, or using sudo to perform the installation.
|
||||||
|
#
|
||||||
|
# $ sudo sh install-docker.sh
|
||||||
|
#
|
||||||
|
# Command-line options
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# --version <VERSION>
|
||||||
|
# Use the --version option to install a specific version, for example:
|
||||||
|
#
|
||||||
|
# $ sudo sh install-docker.sh --version 23.0
|
||||||
|
#
|
||||||
|
# --channel <stable|test>
|
||||||
|
#
|
||||||
|
# Use the --channel option to install from an alternative installation channel.
|
||||||
|
# The following example installs the latest versions from the "test" channel,
|
||||||
|
# which includes pre-releases (alpha, beta, rc):
|
||||||
|
#
|
||||||
|
# $ sudo sh install-docker.sh --channel test
|
||||||
|
#
|
||||||
|
# Alternatively, use the script at https://test.docker.com, which uses the test
|
||||||
|
# channel as default.
|
||||||
|
#
|
||||||
|
# --mirror <Aliyun|AzureChinaCloud>
|
||||||
|
#
|
||||||
|
# Use the --mirror option to install from a mirror supported by this script.
|
||||||
|
# Available mirrors are "Aliyun" (https://mirrors.aliyun.com/docker-ce), and
|
||||||
|
# "AzureChinaCloud" (https://mirror.azure.cn/docker-ce), for example:
|
||||||
|
#
|
||||||
|
# $ sudo sh install-docker.sh --mirror AzureChinaCloud
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# Git commit from https://github.com/docker/docker-install when
|
||||||
|
# the script was uploaded (Should only be modified by upload job):
|
||||||
|
SCRIPT_COMMIT_SHA="4c94a56999e10efcf48c5b8e3f6afea464f9108e"
|
||||||
|
|
||||||
|
# strip "v" prefix if present
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
|
||||||
|
# The channel to install from:
|
||||||
|
# * stable
|
||||||
|
# * test
|
||||||
|
DEFAULT_CHANNEL_VALUE="stable"
|
||||||
|
if [ -z "$CHANNEL" ]; then
|
||||||
|
CHANNEL=$DEFAULT_CHANNEL_VALUE
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEFAULT_DOWNLOAD_URL="https://download.docker.com"
|
||||||
|
if [ -z "$DOWNLOAD_URL" ]; then
|
||||||
|
DOWNLOAD_URL=$DEFAULT_DOWNLOAD_URL
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEFAULT_REPO_FILE="docker-ce.repo"
|
||||||
|
if [ -z "$REPO_FILE" ]; then
|
||||||
|
REPO_FILE="$DEFAULT_REPO_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mirror=''
|
||||||
|
DRY_RUN=${DRY_RUN:-}
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--channel)
|
||||||
|
CHANNEL="$2"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=1
|
||||||
|
;;
|
||||||
|
--mirror)
|
||||||
|
mirror="$2"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--version)
|
||||||
|
VERSION="${2#v}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "Illegal option $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift $(( $# > 0 ? 1 : 0 ))
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$mirror" in
|
||||||
|
Aliyun)
|
||||||
|
DOWNLOAD_URL="https://mirrors.aliyun.com/docker-ce"
|
||||||
|
;;
|
||||||
|
AzureChinaCloud)
|
||||||
|
DOWNLOAD_URL="https://mirror.azure.cn/docker-ce"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
>&2 echo "unknown mirror '$mirror': use either 'Aliyun', or 'AzureChinaCloud'."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$CHANNEL" in
|
||||||
|
stable|test)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
>&2 echo "unknown CHANNEL '$CHANNEL': use either stable or test."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
command_exists() {
|
||||||
|
command -v "$@" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# version_gte checks if the version specified in $VERSION is at least the given
|
||||||
|
# SemVer (Maj.Minor[.Patch]), or CalVer (YY.MM) version.It returns 0 (success)
|
||||||
|
# if $VERSION is either unset (=latest) or newer or equal than the specified
|
||||||
|
# version, or returns 1 (fail) otherwise.
|
||||||
|
#
|
||||||
|
# examples:
|
||||||
|
#
|
||||||
|
# VERSION=23.0
|
||||||
|
# version_gte 23.0 // 0 (success)
|
||||||
|
# version_gte 20.10 // 0 (success)
|
||||||
|
# version_gte 19.03 // 0 (success)
|
||||||
|
# version_gte 26.1 // 1 (fail)
|
||||||
|
version_gte() {
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
version_compare "$VERSION" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# version_compare compares two version strings (either SemVer (Major.Minor.Path),
|
||||||
|
# or CalVer (YY.MM) version strings. It returns 0 (success) if version A is newer
|
||||||
|
# or equal than version B, or 1 (fail) otherwise. Patch releases and pre-release
|
||||||
|
# (-alpha/-beta) are not taken into account
|
||||||
|
#
|
||||||
|
# examples:
|
||||||
|
#
|
||||||
|
# version_compare 23.0.0 20.10 // 0 (success)
|
||||||
|
# version_compare 23.0 20.10 // 0 (success)
|
||||||
|
# version_compare 20.10 19.03 // 0 (success)
|
||||||
|
# version_compare 20.10 20.10 // 0 (success)
|
||||||
|
# version_compare 19.03 20.10 // 1 (fail)
|
||||||
|
version_compare() (
|
||||||
|
set +x
|
||||||
|
|
||||||
|
yy_a="$(echo "$1" | cut -d'.' -f1)"
|
||||||
|
yy_b="$(echo "$2" | cut -d'.' -f1)"
|
||||||
|
if [ "$yy_a" -lt "$yy_b" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ "$yy_a" -gt "$yy_b" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mm_a="$(echo "$1" | cut -d'.' -f2)"
|
||||||
|
mm_b="$(echo "$2" | cut -d'.' -f2)"
|
||||||
|
|
||||||
|
# trim leading zeros to accommodate CalVer
|
||||||
|
mm_a="${mm_a#0}"
|
||||||
|
mm_b="${mm_b#0}"
|
||||||
|
|
||||||
|
if [ "${mm_a:-0}" -lt "${mm_b:-0}" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
)
|
||||||
|
|
||||||
|
is_dry_run() {
|
||||||
|
if [ -z "$DRY_RUN" ]; then
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() {
|
||||||
|
case "$(uname -r)" in
|
||||||
|
*microsoft* ) true ;; # WSL 2
|
||||||
|
*Microsoft* ) true ;; # WSL 1
|
||||||
|
* ) false;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_darwin() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
*darwin* ) true ;;
|
||||||
|
*Darwin* ) true ;;
|
||||||
|
* ) false;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
deprecation_notice() {
|
||||||
|
distro=$1
|
||||||
|
distro_version=$2
|
||||||
|
echo
|
||||||
|
printf "\033[91;1mDEPRECATION WARNING\033[0m\n"
|
||||||
|
printf " This Linux distribution (\033[1m%s %s\033[0m) reached end-of-life and is no longer supported by this script.\n" "$distro" "$distro_version"
|
||||||
|
echo " No updates or security fixes will be released for this distribution, and users are recommended"
|
||||||
|
echo " to upgrade to a currently maintained version of $distro."
|
||||||
|
echo
|
||||||
|
printf "Press \033[1mCtrl+C\033[0m now to abort this script, or wait for the installation to continue."
|
||||||
|
echo
|
||||||
|
sleep 10
|
||||||
|
}
|
||||||
|
|
||||||
|
get_distribution() {
|
||||||
|
lsb_dist=""
|
||||||
|
# Every system that we officially support has /etc/os-release
|
||||||
|
if [ -r /etc/os-release ]; then
|
||||||
|
lsb_dist="$(. /etc/os-release && echo "$ID")"
|
||||||
|
fi
|
||||||
|
# Returning an empty string here should be alright since the
|
||||||
|
# case statements don't act unless you provide an actual value
|
||||||
|
echo "$lsb_dist"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo_docker_as_nonroot() {
|
||||||
|
if is_dry_run; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command_exists docker && [ -e /var/run/docker.sock ]; then
|
||||||
|
(
|
||||||
|
set -x
|
||||||
|
$sh_c 'docker version'
|
||||||
|
) || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# intentionally mixed spaces and tabs here -- tabs are stripped by "<<-EOF", spaces are kept in the output
|
||||||
|
echo
|
||||||
|
echo "================================================================================"
|
||||||
|
echo
|
||||||
|
if version_gte "20.10"; then
|
||||||
|
echo "To run Docker as a non-privileged user, consider setting up the"
|
||||||
|
echo "Docker daemon in rootless mode for your user:"
|
||||||
|
echo
|
||||||
|
echo " dockerd-rootless-setuptool.sh install"
|
||||||
|
echo
|
||||||
|
echo "Visit https://docs.docker.com/go/rootless/ to learn about rootless mode."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "To run the Docker daemon as a fully privileged service, but granting non-root"
|
||||||
|
echo "users access, refer to https://docs.docker.com/go/daemon-access/"
|
||||||
|
echo
|
||||||
|
echo "WARNING: Access to the remote API on a privileged Docker daemon is equivalent"
|
||||||
|
echo " to root access on the host. Refer to the 'Docker daemon attack surface'"
|
||||||
|
echo " documentation for details: https://docs.docker.com/go/attack-surface/"
|
||||||
|
echo
|
||||||
|
echo "================================================================================"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if this is a forked Linux distro
|
||||||
|
check_forked() {
|
||||||
|
|
||||||
|
# Check for lsb_release command existence, it usually exists in forked distros
|
||||||
|
if command_exists lsb_release; then
|
||||||
|
# Check if the `-u` option is supported
|
||||||
|
set +e
|
||||||
|
lsb_release -a -u > /dev/null 2>&1
|
||||||
|
lsb_release_exit_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Check if the command has exited successfully, it means we're in a forked distro
|
||||||
|
if [ "$lsb_release_exit_code" = "0" ]; then
|
||||||
|
# Print info about current distro
|
||||||
|
cat <<-EOF
|
||||||
|
You're using '$lsb_dist' version '$dist_version'.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Get the upstream release info
|
||||||
|
lsb_dist=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')
|
||||||
|
dist_version=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')
|
||||||
|
|
||||||
|
# Print info about upstream distro
|
||||||
|
cat <<-EOF
|
||||||
|
Upstream release is '$lsb_dist' version '$dist_version'.
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then
|
||||||
|
if [ "$lsb_dist" = "osmc" ]; then
|
||||||
|
# OSMC runs Raspbian
|
||||||
|
lsb_dist=raspbian
|
||||||
|
else
|
||||||
|
# We're Debian and don't even know it!
|
||||||
|
lsb_dist=debian
|
||||||
|
fi
|
||||||
|
dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
|
||||||
|
case "$dist_version" in
|
||||||
|
12)
|
||||||
|
dist_version="bookworm"
|
||||||
|
;;
|
||||||
|
11)
|
||||||
|
dist_version="bullseye"
|
||||||
|
;;
|
||||||
|
10)
|
||||||
|
dist_version="buster"
|
||||||
|
;;
|
||||||
|
9)
|
||||||
|
dist_version="stretch"
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
dist_version="jessie"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
do_install() {
|
||||||
|
echo "# Executing docker install script, commit: $SCRIPT_COMMIT_SHA"
|
||||||
|
|
||||||
|
if command_exists docker; then
|
||||||
|
cat >&2 <<-'EOF'
|
||||||
|
Warning: the "docker" command appears to already exist on this system.
|
||||||
|
|
||||||
|
If you already have Docker installed, this script can cause trouble, which is
|
||||||
|
why we're displaying this warning and provide the opportunity to cancel the
|
||||||
|
installation.
|
||||||
|
|
||||||
|
If you installed the current Docker package using this script and are using it
|
||||||
|
again to update Docker, you can ignore this message, but be aware that the
|
||||||
|
script resets any custom changes in the deb and rpm repo configuration
|
||||||
|
files to match the parameters passed to the script.
|
||||||
|
|
||||||
|
You may press Ctrl+C now to abort this script.
|
||||||
|
EOF
|
||||||
|
( set -x; sleep 20 )
|
||||||
|
fi
|
||||||
|
|
||||||
|
user="$(id -un 2>/dev/null || true)"
|
||||||
|
|
||||||
|
sh_c='sh -c'
|
||||||
|
if [ "$user" != 'root' ]; then
|
||||||
|
if command_exists sudo; then
|
||||||
|
sh_c='sudo -E sh -c'
|
||||||
|
elif command_exists su; then
|
||||||
|
sh_c='su -c'
|
||||||
|
else
|
||||||
|
cat >&2 <<-'EOF'
|
||||||
|
Error: this installer needs the ability to run commands as root.
|
||||||
|
We are unable to find either "sudo" or "su" available to make this happen.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_dry_run; then
|
||||||
|
sh_c="echo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# perform some very rudimentary platform detection
|
||||||
|
lsb_dist=$( get_distribution )
|
||||||
|
lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
|
if is_wsl; then
|
||||||
|
echo
|
||||||
|
echo "WSL DETECTED: We recommend using Docker Desktop for Windows."
|
||||||
|
echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop/"
|
||||||
|
echo
|
||||||
|
cat >&2 <<-'EOF'
|
||||||
|
|
||||||
|
You may press Ctrl+C now to abort this script.
|
||||||
|
EOF
|
||||||
|
( set -x; sleep 20 )
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$lsb_dist" in
|
||||||
|
|
||||||
|
ubuntu)
|
||||||
|
if command_exists lsb_release; then
|
||||||
|
dist_version="$(lsb_release --codename | cut -f2)"
|
||||||
|
fi
|
||||||
|
if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then
|
||||||
|
dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
debian|raspbian)
|
||||||
|
dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
|
||||||
|
case "$dist_version" in
|
||||||
|
12)
|
||||||
|
dist_version="bookworm"
|
||||||
|
;;
|
||||||
|
11)
|
||||||
|
dist_version="bullseye"
|
||||||
|
;;
|
||||||
|
10)
|
||||||
|
dist_version="buster"
|
||||||
|
;;
|
||||||
|
9)
|
||||||
|
dist_version="stretch"
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
dist_version="jessie"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
|
||||||
|
centos|rhel)
|
||||||
|
if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
|
||||||
|
dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
if command_exists lsb_release; then
|
||||||
|
dist_version="$(lsb_release --release | cut -f2)"
|
||||||
|
fi
|
||||||
|
if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
|
||||||
|
dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if this is a forked Linux distro
|
||||||
|
check_forked
|
||||||
|
|
||||||
|
# Print deprecation warnings for distro versions that recently reached EOL,
|
||||||
|
# but may still be commonly used (especially LTS versions).
|
||||||
|
case "$lsb_dist.$dist_version" in
|
||||||
|
centos.8|centos.7|rhel.7)
|
||||||
|
deprecation_notice "$lsb_dist" "$dist_version"
|
||||||
|
;;
|
||||||
|
debian.buster|debian.stretch|debian.jessie)
|
||||||
|
deprecation_notice "$lsb_dist" "$dist_version"
|
||||||
|
;;
|
||||||
|
raspbian.buster|raspbian.stretch|raspbian.jessie)
|
||||||
|
deprecation_notice "$lsb_dist" "$dist_version"
|
||||||
|
;;
|
||||||
|
ubuntu.bionic|ubuntu.xenial|ubuntu.trusty)
|
||||||
|
deprecation_notice "$lsb_dist" "$dist_version"
|
||||||
|
;;
|
||||||
|
ubuntu.mantic|ubuntu.lunar|ubuntu.kinetic|ubuntu.impish|ubuntu.hirsute|ubuntu.groovy|ubuntu.eoan|ubuntu.disco|ubuntu.cosmic)
|
||||||
|
deprecation_notice "$lsb_dist" "$dist_version"
|
||||||
|
;;
|
||||||
|
fedora.*)
|
||||||
|
if [ "$dist_version" -lt 40 ]; then
|
||||||
|
deprecation_notice "$lsb_dist" "$dist_version"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Run setup for each distro accordingly
|
||||||
|
case "$lsb_dist" in
|
||||||
|
ubuntu|debian|raspbian)
|
||||||
|
pre_reqs="ca-certificates curl"
|
||||||
|
apt_repo="deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] $DOWNLOAD_URL/linux/$lsb_dist $dist_version $CHANNEL"
|
||||||
|
(
|
||||||
|
if ! is_dry_run; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
$sh_c 'apt-get -qq update >/dev/null'
|
||||||
|
$sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pre_reqs >/dev/null"
|
||||||
|
$sh_c 'install -m 0755 -d /etc/apt/keyrings'
|
||||||
|
$sh_c "curl -fsSL \"$DOWNLOAD_URL/linux/$lsb_dist/gpg\" -o /etc/apt/keyrings/docker.asc"
|
||||||
|
$sh_c "chmod a+r /etc/apt/keyrings/docker.asc"
|
||||||
|
$sh_c "echo \"$apt_repo\" > /etc/apt/sources.list.d/docker.list"
|
||||||
|
$sh_c 'apt-get -qq update >/dev/null'
|
||||||
|
)
|
||||||
|
pkg_version=""
|
||||||
|
if [ -n "$VERSION" ]; then
|
||||||
|
if is_dry_run; then
|
||||||
|
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||||
|
else
|
||||||
|
# Will work for incomplete versions IE (17.12), but may not actually grab the "latest" if in the test channel
|
||||||
|
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/~ce~.*/g' | sed 's/-/.*/g')"
|
||||||
|
search_command="apt-cache madison docker-ce | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
|
||||||
|
pkg_version="$($sh_c "$search_command")"
|
||||||
|
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||||
|
echo "INFO: $search_command"
|
||||||
|
if [ -z "$pkg_version" ]; then
|
||||||
|
echo
|
||||||
|
echo "ERROR: '$VERSION' not found amongst apt-cache madison results"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if version_gte "18.09"; then
|
||||||
|
search_command="apt-cache madison docker-ce-cli | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
|
||||||
|
echo "INFO: $search_command"
|
||||||
|
cli_pkg_version="=$($sh_c "$search_command")"
|
||||||
|
fi
|
||||||
|
pkg_version="=$pkg_version"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
pkgs="docker-ce${pkg_version%=}"
|
||||||
|
if version_gte "18.09"; then
|
||||||
|
# older versions didn't ship the cli and containerd as separate packages
|
||||||
|
pkgs="$pkgs docker-ce-cli${cli_pkg_version%=} containerd.io"
|
||||||
|
fi
|
||||||
|
if version_gte "20.10"; then
|
||||||
|
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||||
|
fi
|
||||||
|
if version_gte "23.0"; then
|
||||||
|
pkgs="$pkgs docker-buildx-plugin"
|
||||||
|
fi
|
||||||
|
if ! is_dry_run; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
$sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pkgs >/dev/null"
|
||||||
|
)
|
||||||
|
echo_docker_as_nonroot
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
centos|fedora|rhel)
|
||||||
|
repo_file_url="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE"
|
||||||
|
(
|
||||||
|
if ! is_dry_run; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
if command_exists dnf5; then
|
||||||
|
$sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
|
||||||
|
$sh_c "dnf5 config-manager addrepo --overwrite --save-filename=docker-ce.repo --from-repofile='$repo_file_url'"
|
||||||
|
|
||||||
|
if [ "$CHANNEL" != "stable" ]; then
|
||||||
|
$sh_c "dnf5 config-manager setopt \"docker-ce-*.enabled=0\""
|
||||||
|
$sh_c "dnf5 config-manager setopt \"docker-ce-$CHANNEL.enabled=1\""
|
||||||
|
fi
|
||||||
|
$sh_c "dnf makecache"
|
||||||
|
elif command_exists dnf; then
|
||||||
|
$sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
|
||||||
|
$sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo"
|
||||||
|
$sh_c "dnf config-manager --add-repo $repo_file_url"
|
||||||
|
|
||||||
|
if [ "$CHANNEL" != "stable" ]; then
|
||||||
|
$sh_c "dnf config-manager --set-disabled \"docker-ce-*\""
|
||||||
|
$sh_c "dnf config-manager --set-enabled \"docker-ce-$CHANNEL\""
|
||||||
|
fi
|
||||||
|
$sh_c "dnf makecache"
|
||||||
|
else
|
||||||
|
$sh_c "yum -y -q install yum-utils"
|
||||||
|
$sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo"
|
||||||
|
$sh_c "yum-config-manager --add-repo $repo_file_url"
|
||||||
|
|
||||||
|
if [ "$CHANNEL" != "stable" ]; then
|
||||||
|
$sh_c "yum-config-manager --disable \"docker-ce-*\""
|
||||||
|
$sh_c "yum-config-manager --enable \"docker-ce-$CHANNEL\""
|
||||||
|
fi
|
||||||
|
$sh_c "yum makecache"
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
pkg_version=""
|
||||||
|
if command_exists dnf; then
|
||||||
|
pkg_manager="dnf"
|
||||||
|
pkg_manager_flags="-y -q --best"
|
||||||
|
else
|
||||||
|
pkg_manager="yum"
|
||||||
|
pkg_manager_flags="-y -q"
|
||||||
|
fi
|
||||||
|
if [ -n "$VERSION" ]; then
|
||||||
|
if is_dry_run; then
|
||||||
|
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||||
|
else
|
||||||
|
if [ "$lsb_dist" = "fedora" ]; then
|
||||||
|
pkg_suffix="fc$dist_version"
|
||||||
|
else
|
||||||
|
pkg_suffix="el"
|
||||||
|
fi
|
||||||
|
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/\\\\.ce.*/g' | sed 's/-/.*/g').*$pkg_suffix"
|
||||||
|
search_command="$pkg_manager list --showduplicates docker-ce | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
|
||||||
|
pkg_version="$($sh_c "$search_command")"
|
||||||
|
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||||
|
echo "INFO: $search_command"
|
||||||
|
if [ -z "$pkg_version" ]; then
|
||||||
|
echo
|
||||||
|
echo "ERROR: '$VERSION' not found amongst $pkg_manager list results"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if version_gte "18.09"; then
|
||||||
|
# older versions don't support a cli package
|
||||||
|
search_command="$pkg_manager list --showduplicates docker-ce-cli | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
|
||||||
|
cli_pkg_version="$($sh_c "$search_command" | cut -d':' -f 2)"
|
||||||
|
fi
|
||||||
|
# Cut out the epoch and prefix with a '-'
|
||||||
|
pkg_version="-$(echo "$pkg_version" | cut -d':' -f 2)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
pkgs="docker-ce$pkg_version"
|
||||||
|
if version_gte "18.09"; then
|
||||||
|
# older versions didn't ship the cli and containerd as separate packages
|
||||||
|
if [ -n "$cli_pkg_version" ]; then
|
||||||
|
pkgs="$pkgs docker-ce-cli-$cli_pkg_version containerd.io"
|
||||||
|
else
|
||||||
|
pkgs="$pkgs docker-ce-cli containerd.io"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if version_gte "20.10"; then
|
||||||
|
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||||
|
fi
|
||||||
|
if version_gte "23.0"; then
|
||||||
|
pkgs="$pkgs docker-buildx-plugin"
|
||||||
|
fi
|
||||||
|
if ! is_dry_run; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
$sh_c "$pkg_manager $pkg_manager_flags install $pkgs"
|
||||||
|
)
|
||||||
|
echo_docker_as_nonroot
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
sles)
|
||||||
|
if [ "$(uname -m)" != "s390x" ]; then
|
||||||
|
echo "Packages for SLES are currently only available for s390x"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
repo_file_url="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE"
|
||||||
|
pre_reqs="ca-certificates curl libseccomp2 awk"
|
||||||
|
(
|
||||||
|
if ! is_dry_run; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
$sh_c "zypper install -y $pre_reqs"
|
||||||
|
$sh_c "rm -f /etc/zypp/repos.d/docker-ce-*.repo"
|
||||||
|
$sh_c "zypper addrepo $repo_file_url"
|
||||||
|
|
||||||
|
opensuse_factory_url="https://download.opensuse.org/repositories/security:/SELinux/openSUSE_Factory/"
|
||||||
|
if ! zypper lr -d | grep -q "${opensuse_factory_url}"; then
|
||||||
|
opensuse_repo="${opensuse_factory_url}security:SELinux.repo"
|
||||||
|
if ! is_dry_run; then
|
||||||
|
cat >&2 <<- EOF
|
||||||
|
WARNING!!
|
||||||
|
openSUSE repository ($opensuse_repo) will be enabled now.
|
||||||
|
Do you wish to continue?
|
||||||
|
You may press Ctrl+C now to abort this script.
|
||||||
|
EOF
|
||||||
|
( set -x; sleep 20 )
|
||||||
|
fi
|
||||||
|
$sh_c "zypper addrepo $opensuse_repo"
|
||||||
|
fi
|
||||||
|
$sh_c "zypper --gpg-auto-import-keys refresh"
|
||||||
|
$sh_c "zypper lr -d"
|
||||||
|
)
|
||||||
|
pkg_version=""
|
||||||
|
if [ -n "$VERSION" ]; then
|
||||||
|
if is_dry_run; then
|
||||||
|
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||||
|
else
|
||||||
|
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/\\\\.ce.*/g' | sed 's/-/.*/g')"
|
||||||
|
search_command="zypper search -s --match-exact 'docker-ce' | grep '$pkg_pattern' | tail -1 | awk '{print \$6}'"
|
||||||
|
pkg_version="$($sh_c "$search_command")"
|
||||||
|
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||||
|
echo "INFO: $search_command"
|
||||||
|
if [ -z "$pkg_version" ]; then
|
||||||
|
echo
|
||||||
|
echo "ERROR: '$VERSION' not found amongst zypper list results"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
search_command="zypper search -s --match-exact 'docker-ce-cli' | grep '$pkg_pattern' | tail -1 | awk '{print \$6}'"
|
||||||
|
# It's okay for cli_pkg_version to be blank, since older versions don't support a cli package
|
||||||
|
cli_pkg_version="$($sh_c "$search_command")"
|
||||||
|
pkg_version="-$pkg_version"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
pkgs="docker-ce$pkg_version"
|
||||||
|
if version_gte "18.09"; then
|
||||||
|
if [ -n "$cli_pkg_version" ]; then
|
||||||
|
# older versions didn't ship the cli and containerd as separate packages
|
||||||
|
pkgs="$pkgs docker-ce-cli-$cli_pkg_version containerd.io"
|
||||||
|
else
|
||||||
|
pkgs="$pkgs docker-ce-cli containerd.io"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if version_gte "20.10"; then
|
||||||
|
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||||
|
fi
|
||||||
|
if version_gte "23.0"; then
|
||||||
|
pkgs="$pkgs docker-buildx-plugin"
|
||||||
|
fi
|
||||||
|
if ! is_dry_run; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
$sh_c "zypper -q install -y $pkgs"
|
||||||
|
)
|
||||||
|
echo_docker_as_nonroot
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "$lsb_dist" ]; then
|
||||||
|
if is_darwin; then
|
||||||
|
echo
|
||||||
|
echo "ERROR: Unsupported operating system 'macOS'"
|
||||||
|
echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "ERROR: Unsupported distribution '$lsb_dist'"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# wrapped up in a function so that we have some protection against only getting
|
||||||
|
# half the file during "curl | sh"
|
||||||
|
do_install
|
376
https-setup.sh
Executable file
376
https-setup.sh
Executable file
@ -0,0 +1,376 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# HTTPS-Setup-Skript für das MYP-Projekt
|
||||||
|
# Konfiguriert einen Raspberry Pi mit einer dual-network-Konfiguration
|
||||||
|
# - LAN (eth0): Firmennetzwerk mit Zugang zu Internet und unter https://m040tbaraspi001.de040.corpintra.net/ erreichbar
|
||||||
|
# - WLAN (wlan0): Verbindung zum Offline-Netzwerk, wo der Backend-Host erreichbar ist
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktion zur Ausgabe mit Zeitstempel
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
success_log() {
|
||||||
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ERFOLG:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}===== $1 =====${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfen, ob das Skript als Root ausgeführt wird
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
error_log "Dieses Skript muss als Root ausgeführt werden."
|
||||||
|
error_log "Bitte führen Sie es mit 'sudo' aus."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfigurationswerte
|
||||||
|
FIRMENNETZWERK_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||||
|
BACKEND_HOST="192.168.0.105" # Backend-IP im Offline-Netzwerk
|
||||||
|
BACKEND_PORT="5000" # Backend-Port
|
||||||
|
OFFLINE_NETWORK_SSID="MYP-Net"
|
||||||
|
OFFLINE_NETWORK_PASSWORD="myp-password"
|
||||||
|
CADDY_VERSION="2.7.6"
|
||||||
|
|
||||||
|
header "MYP-Netzwerk und HTTPS-Setup"
|
||||||
|
log "Dieses Skript konfiguriert Ihren Raspberry Pi für:"
|
||||||
|
log "1. Firmennetzwerk über LAN (eth0) mit Internet-Zugang"
|
||||||
|
log "2. Offline-Netzwerk über WLAN (wlan0) für Backend-Kommunikation"
|
||||||
|
log "3. HTTPS mit selbstsigniertem Zertifikat für ${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
|
||||||
|
# Netzwerkkonfiguration
|
||||||
|
setup_network() {
|
||||||
|
header "Netzwerkkonfiguration"
|
||||||
|
|
||||||
|
# Sichern der aktuellen Netzwerkkonfiguration
|
||||||
|
log "Sichere aktuelle Netzwerkkonfiguration..."
|
||||||
|
if [ -f /etc/dhcpcd.conf ]; then
|
||||||
|
cp /etc/dhcpcd.conf /etc/dhcpcd.conf.bak
|
||||||
|
success_log "Aktuelle Netzwerkkonfiguration gesichert in /etc/dhcpcd.conf.bak"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfigurieren von dhcpcd.conf für statische Routing
|
||||||
|
log "Konfiguriere Routing für duale Netzwerke..."
|
||||||
|
cat > /etc/dhcpcd.conf << EOL
|
||||||
|
# MYP Dual-Network Configuration
|
||||||
|
# eth0: Firmennetzwerk mit Internet
|
||||||
|
# wlan0: Offline-Netzwerk für Backend
|
||||||
|
|
||||||
|
# Allow dhcpcd to manage all interfaces
|
||||||
|
allowinterfaces eth0 wlan0
|
||||||
|
|
||||||
|
# eth0 configuration (Firmennetzwerk)
|
||||||
|
interface eth0
|
||||||
|
# DHCP for eth0, all default routes go through eth0
|
||||||
|
metric 100
|
||||||
|
|
||||||
|
# wlan0 configuration (Offline Network)
|
||||||
|
interface wlan0
|
||||||
|
# Static IP for wlan0
|
||||||
|
metric 200
|
||||||
|
# Add specific route to backend via wlan0
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Konfigurieren von wpa_supplicant für WLAN-Verbindung
|
||||||
|
log "Konfiguriere WLAN-Verbindung für Offline-Netzwerk..."
|
||||||
|
cat > /etc/wpa_supplicant/wpa_supplicant.conf << EOL
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
country=DE
|
||||||
|
|
||||||
|
network={
|
||||||
|
ssid="${OFFLINE_NETWORK_SSID}"
|
||||||
|
psk="${OFFLINE_NETWORK_PASSWORD}"
|
||||||
|
priority=1
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf
|
||||||
|
|
||||||
|
# Routing-Tabelle konfigurieren
|
||||||
|
log "Konfiguriere spezifisches Routing zum Backend..."
|
||||||
|
if ! grep -q "${BACKEND_HOST}" /etc/iproute2/rt_tables; then
|
||||||
|
echo "200 backend" >> /etc/iproute2/rt_tables
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Routing-Regeln in /etc/network/if-up.d/ hinzufügen
|
||||||
|
cat > /etc/network/if-up.d/route-backend << EOL
|
||||||
|
#!/bin/bash
|
||||||
|
# Routing-Regeln für Backend-Host über WLAN
|
||||||
|
|
||||||
|
# Wenn wlan0 hochgefahren wird
|
||||||
|
if [ "\$IFACE" = "wlan0" ]; then
|
||||||
|
# Spezifische Route zum Backend über wlan0
|
||||||
|
/sbin/ip route add ${BACKEND_HOST}/32 dev wlan0
|
||||||
|
fi
|
||||||
|
EOL
|
||||||
|
|
||||||
|
chmod +x /etc/network/if-up.d/route-backend
|
||||||
|
|
||||||
|
success_log "Netzwerkkonfiguration abgeschlossen"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installation von Caddy als Reverse-Proxy
|
||||||
|
install_caddy() {
|
||||||
|
header "Installation von Caddy als Reverse-Proxy"
|
||||||
|
|
||||||
|
log "Überprüfe, ob Caddy bereits installiert ist..."
|
||||||
|
if command -v caddy &> /dev/null; then
|
||||||
|
success_log "Caddy ist bereits installiert"
|
||||||
|
else
|
||||||
|
log "Installiere Caddy ${CADDY_VERSION}..."
|
||||||
|
|
||||||
|
# Download und Installation von Caddy
|
||||||
|
wget -O /tmp/caddy.tar.gz "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_armv7.tar.gz"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Fehler beim Herunterladen von Caddy"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -xzf /tmp/caddy.tar.gz -C /tmp
|
||||||
|
mv /tmp/caddy /usr/local/bin/
|
||||||
|
chmod +x /usr/local/bin/caddy
|
||||||
|
|
||||||
|
# Benutzer und Gruppe für Caddy erstellen
|
||||||
|
if ! id -u caddy &>/dev/null; then
|
||||||
|
useradd --system --home /var/lib/caddy --shell /usr/sbin/nologin caddy
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verzeichnisse für Caddy erstellen
|
||||||
|
mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy
|
||||||
|
chown -R caddy:caddy /var/lib/caddy /var/log/caddy
|
||||||
|
|
||||||
|
# Systemd-Service für Caddy einrichten
|
||||||
|
cat > /etc/systemd/system/caddy.service << EOL
|
||||||
|
[Unit]
|
||||||
|
Description=Caddy Web Server
|
||||||
|
Documentation=https://caddyserver.com/docs/
|
||||||
|
After=network.target network-online.target
|
||||||
|
Requires=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=caddy
|
||||||
|
Group=caddy
|
||||||
|
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
|
||||||
|
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
TimeoutStopSec=5s
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
LimitNPROC=512
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
success_log "Caddy wurde installiert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Caddyfile für HTTPS mit selbstsigniertem Zertifikat konfigurieren
|
||||||
|
log "Konfiguriere Caddy für HTTPS mit selbstsigniertem Zertifikat..."
|
||||||
|
cat > /etc/caddy/Caddyfile << EOL
|
||||||
|
{
|
||||||
|
# Globale Optionen
|
||||||
|
admin off
|
||||||
|
auto_https disable_redirects
|
||||||
|
|
||||||
|
# Selbstsigniertes Zertifikat verwenden
|
||||||
|
local_certs
|
||||||
|
default_sni ${FIRMENNETZWERK_HOSTNAME}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS-Konfiguration für den Firmennetzwerk-Hostnamen
|
||||||
|
${FIRMENNETZWERK_HOSTNAME} {
|
||||||
|
# TLS mit selbstsigniertem Zertifikat
|
||||||
|
tls internal {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse-Proxy zum Next.js-Frontend
|
||||||
|
reverse_proxy localhost:3000 {
|
||||||
|
# Zeitüberschreitungen für langsame Raspberry Pi-Verbindungen erhöhen
|
||||||
|
timeouts 5m
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/access.log
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP-Konfiguration für lokalen Zugriff
|
||||||
|
:80 {
|
||||||
|
# Weiterleitung zu HTTPS
|
||||||
|
redir https://${FIRMENNETZWERK_HOSTNAME}{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zusätzlicher Server für Backend-Proxy (API-Anfragen weiterleiten)
|
||||||
|
localhost:8000 {
|
||||||
|
reverse_proxy ${BACKEND_HOST}:${BACKEND_PORT} {
|
||||||
|
# Headers für CORS anpassen
|
||||||
|
header_up Host ${BACKEND_HOST}:${BACKEND_PORT}
|
||||||
|
header_up X-Forwarded-Host ${FIRMENNETZWERK_HOSTNAME}
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
|
|
||||||
|
# Zeitüberschreitungen für API-Anfragen erhöhen
|
||||||
|
timeouts 1m
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/backend-access.log
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Caddy-Service neu laden und starten
|
||||||
|
log "Starte Caddy-Service..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable caddy
|
||||||
|
systemctl restart caddy
|
||||||
|
|
||||||
|
# Überprüfen, ob Caddy läuft
|
||||||
|
if systemctl is-active --quiet caddy; then
|
||||||
|
success_log "Caddy-Service wurde gestartet und ist aktiv"
|
||||||
|
else
|
||||||
|
error_log "Fehler beim Starten des Caddy-Services"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next.js Frontend-Konfiguration für HTTPS und Backend-Proxy
|
||||||
|
configure_frontend() {
|
||||||
|
header "Frontend-Konfiguration für HTTPS"
|
||||||
|
|
||||||
|
# Verzeichnis für das Frontend
|
||||||
|
FRONTEND_DIR="/home/kasm-user/Desktop/Projektarbeit-MYP/packages/reservation-platform"
|
||||||
|
|
||||||
|
# Prüfen, ob das Frontend-Verzeichnis existiert
|
||||||
|
if [ ! -d "$FRONTEND_DIR" ]; then
|
||||||
|
error_log "Frontend-Verzeichnis nicht gefunden: $FRONTEND_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Konfiguriere Frontend für HTTPS und Backend-Proxy..."
|
||||||
|
|
||||||
|
# .env.local-Datei für das Frontend erstellen
|
||||||
|
cat > "$FRONTEND_DIR/.env.local" << EOL
|
||||||
|
# Backend API Konfiguration (über lokalen Proxy zu Backend)
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# Frontend-URL für OAuth Callback (HTTPS)
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=https://${FIRMENNETZWERK_HOSTNAME}
|
||||||
|
|
||||||
|
# Explizite OAuth Callback URL für GitHub
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=https://${FIRMENNETZWERK_HOSTNAME}/auth/login/callback
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Berechtigungen setzen
|
||||||
|
chown -R $(stat -c '%U:%G' "$FRONTEND_DIR") "$FRONTEND_DIR/.env.local"
|
||||||
|
chmod 600 "$FRONTEND_DIR/.env.local"
|
||||||
|
|
||||||
|
success_log "Frontend wurde für HTTPS und Backend-Proxy konfiguriert"
|
||||||
|
|
||||||
|
# Hinweis zur Installation und zum Start des Frontends
|
||||||
|
log "${YELLOW}Hinweis: Führen Sie nun das Frontend-Deployment-Skript aus:${NC}"
|
||||||
|
log "cd /home/kasm-user/Desktop/Projektarbeit-MYP && ./raspi-frontend-deploy.sh"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hostname setzen
|
||||||
|
set_hostname() {
|
||||||
|
header "Hostname konfigurieren"
|
||||||
|
|
||||||
|
log "Aktueller Hostname: $(hostname)"
|
||||||
|
log "Setze Hostname auf ${FIRMENNETZWERK_HOSTNAME}..."
|
||||||
|
|
||||||
|
# Hostname in /etc/hostname setzen
|
||||||
|
echo "${FIRMENNETZWERK_HOSTNAME}" > /etc/hostname
|
||||||
|
|
||||||
|
# Hostname in /etc/hosts aktualisieren
|
||||||
|
if grep -q "$(hostname)" /etc/hosts; then
|
||||||
|
sed -i "s/$(hostname)/${FIRMENNETZWERK_HOSTNAME}/g" /etc/hosts
|
||||||
|
else
|
||||||
|
echo "127.0.1.1 ${FIRMENNETZWERK_HOSTNAME}" >> /etc/hosts
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktualisieren des Hostnamens ohne Neustart
|
||||||
|
hostname "${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
|
||||||
|
success_log "Hostname wurde auf ${FIRMENNETZWERK_HOSTNAME} gesetzt"
|
||||||
|
log "${YELLOW}Hinweis: Ein Neustart wird empfohlen, um sicherzustellen, dass der neue Hostname vollständig übernommen wurde.${NC}"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hauptfunktion
|
||||||
|
main() {
|
||||||
|
# Begrüßung und Bestätigung
|
||||||
|
header "MYP HTTPS und Dual-Network Setup"
|
||||||
|
log "Dieses Skript richtet Ihren Raspberry Pi für das MYP-Projekt ein:"
|
||||||
|
log "- Setzt den Hostname auf: ${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
log "- Konfiguriert das duale Netzwerk (LAN für Internet, WLAN für Backend)"
|
||||||
|
log "- Installiert Caddy als Reverse-Proxy mit selbstsigniertem HTTPS"
|
||||||
|
log "- Konfiguriert das Frontend für die Kommunikation mit dem Backend"
|
||||||
|
echo ""
|
||||||
|
log "${YELLOW}WICHTIG: Diese Konfiguration kann bestehende Netzwerk- und HTTPS-Einstellungen überschreiben.${NC}"
|
||||||
|
read -p "Möchten Sie fortfahren? (j/n): " confirm
|
||||||
|
|
||||||
|
if [[ "$confirm" != "j" ]]; then
|
||||||
|
log "Setup abgebrochen."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Schritte ausführen
|
||||||
|
set_hostname || { error_log "Fehler beim Setzen des Hostnamens"; exit 1; }
|
||||||
|
setup_network || { error_log "Fehler bei der Netzwerkkonfiguration"; exit 1; }
|
||||||
|
install_caddy || { error_log "Fehler bei der Caddy-Installation"; exit 1; }
|
||||||
|
configure_frontend || { error_log "Fehler bei der Frontend-Konfiguration"; exit 1; }
|
||||||
|
|
||||||
|
# Abschlussmeldung
|
||||||
|
header "Setup abgeschlossen"
|
||||||
|
success_log "MYP HTTPS und Dual-Network Setup erfolgreich!"
|
||||||
|
log "Ihr Raspberry Pi ist nun wie folgt konfiguriert:"
|
||||||
|
log "- Hostname: ${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
log "- HTTPS mit selbstsigniertem Zertifikat über Caddy"
|
||||||
|
log "- Duale Netzwerkkonfiguration:"
|
||||||
|
log " * eth0: Firmennetzwerk mit Internet-Zugang"
|
||||||
|
log " * wlan0: Verbindung zum Offline-Netzwerk (${OFFLINE_NETWORK_SSID})"
|
||||||
|
log "- Frontend-URL: https://${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
log "- Backend-Kommunikation über lokalen Proxy: http://localhost:8000 -> ${BACKEND_HOST}:${BACKEND_PORT}"
|
||||||
|
echo ""
|
||||||
|
log "${YELLOW}Wichtige nächste Schritte:${NC}"
|
||||||
|
log "1. Starten Sie das Frontend mit dem Deployment-Skript:"
|
||||||
|
log " cd /home/kasm-user/Desktop/Projektarbeit-MYP && ./raspi-frontend-deploy.sh"
|
||||||
|
log "2. Neustart des Raspberry Pi empfohlen:"
|
||||||
|
log " sudo reboot"
|
||||||
|
echo ""
|
||||||
|
log "${YELLOW}Hinweis zum selbstsignierten Zertifikat:${NC}"
|
||||||
|
log "Bei Zugriff auf https://${FIRMENNETZWERK_HOSTNAME} erhalten Sie eine Zertifikatswarnung im Browser."
|
||||||
|
log "Dies ist normal für selbstsignierte Zertifikate. Sie können die Warnung in Ihrem Browser bestätigen."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skript starten
|
||||||
|
main
|
516
install-backend.sh
Executable file
516
install-backend.sh
Executable file
@ -0,0 +1,516 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# MYP Backend Installations-Skript
|
||||||
|
# Dieses Skript installiert das Backend mit Docker und Host-Netzwerkanbindung
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktion zur Ausgabe mit Zeitstempel
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Bereinigen vorhandener Installationen
|
||||||
|
cleanup_existing_installation() {
|
||||||
|
log "${YELLOW}Bereinige vorhandene Installation...${NC}"
|
||||||
|
|
||||||
|
# Stoppe und entferne existierende Container
|
||||||
|
if docker ps -a | grep -q "myp-backend"; then
|
||||||
|
log "Stoppe und entferne existierenden Backend-Container..."
|
||||||
|
docker stop myp-backend &>/dev/null || true
|
||||||
|
docker rm myp-backend &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Entferne Docker Images
|
||||||
|
if docker images | grep -q "myp-backend"; then
|
||||||
|
log "Entferne existierendes Backend-Image..."
|
||||||
|
docker rmi myp-backend &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}Bereinigung abgeschlossen.${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pfade definieren
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
BACKEND_DIR="$SCRIPT_DIR/backend"
|
||||||
|
|
||||||
|
# Prüfen ob Verzeichnis existiert
|
||||||
|
if [ ! -d "$BACKEND_DIR" ]; then
|
||||||
|
error_log "Backend-Verzeichnis '$BACKEND_DIR' nicht gefunden."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bereinige existierende Installation
|
||||||
|
cleanup_existing_installation
|
||||||
|
|
||||||
|
# Funktion zur Installation von Docker und Docker Compose für Raspberry Pi
|
||||||
|
install_docker() {
|
||||||
|
log "${YELLOW}Docker ist nicht installiert. Installation wird gestartet...${NC}"
|
||||||
|
|
||||||
|
# Erkenne Raspberry Pi
|
||||||
|
if [ -f /proc/device-tree/model ] && grep -q "Raspberry Pi" /proc/device-tree/model; then
|
||||||
|
log "${GREEN}Raspberry Pi erkannt. Installiere Docker für ARM-Architektur...${NC}"
|
||||||
|
IS_RASPBERRY_PI=true
|
||||||
|
else
|
||||||
|
IS_RASPBERRY_PI=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktualisiere Paketindex
|
||||||
|
if ! sudo apt-get update; then
|
||||||
|
error_log "Konnte Paketindex nicht aktualisieren. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installiere erforderliche Pakete
|
||||||
|
if ! sudo apt-get install -y apt-transport-https ca-certificates curl gnupg software-properties-common; then
|
||||||
|
error_log "Konnte erforderliche Pakete nicht installieren. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Raspberry Pi-spezifische Installation
|
||||||
|
if [ "$IS_RASPBERRY_PI" = true ]; then
|
||||||
|
# Setze Systemarchitektur für Raspberry Pi (armhf oder arm64)
|
||||||
|
ARCH=$(dpkg --print-architecture)
|
||||||
|
log "Erkannte Systemarchitektur: ${ARCH}"
|
||||||
|
|
||||||
|
# Installiere Docker mit convenience script (für Raspberry Pi empfohlen)
|
||||||
|
log "${YELLOW}Installiere Docker mit dem convenience script...${NC}"
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Docker-Installation fehlgeschlagen. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Standard-Installation für andere Systeme
|
||||||
|
# Füge Docker's offiziellen GPG-Schlüssel hinzu
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||||
|
|
||||||
|
# Füge Docker-Repository hinzu
|
||||||
|
if ! sudo add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"; then
|
||||||
|
error_log "Konnte Docker-Repository nicht hinzufügen. Prüfen Sie, ob Ihr System unterstützt wird."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktualisiere Paketindex erneut
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
# Installiere Docker
|
||||||
|
if ! sudo apt-get install -y docker-ce docker-ce-cli containerd.io; then
|
||||||
|
error_log "Konnte Docker nicht installieren. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Füge aktuellen Benutzer zur Docker-Gruppe hinzu
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
|
||||||
|
log "${GREEN}Docker wurde installiert.${NC}"
|
||||||
|
log "${YELLOW}WICHTIG: Möglicherweise müssen Sie sich neu anmelden, damit die Gruppenänderung wirksam wird.${NC}"
|
||||||
|
|
||||||
|
# Prüfen, ob Docker Compose v2 Plugin verfügbar ist (bevorzugt, da moderner)
|
||||||
|
log "${YELLOW}Prüfe Docker Compose Version...${NC}"
|
||||||
|
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
log "${GREEN}Docker Compose v2 Plugin ist bereits installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=true
|
||||||
|
else
|
||||||
|
log "${YELLOW}Docker Compose v2 Plugin nicht gefunden. Versuche Docker Compose v1 zu installieren...${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=false
|
||||||
|
|
||||||
|
if [ "$IS_RASPBERRY_PI" = true ]; then
|
||||||
|
# Für Raspberry Pi ist es besser, die richtige Architektur zu verwenden
|
||||||
|
if [ "$ARCH" = "armhf" ]; then
|
||||||
|
log "Installiere Docker Compose für armhf (32-bit)..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose
|
||||||
|
elif [ "$ARCH" = "arm64" ]; then
|
||||||
|
log "Installiere Docker Compose für arm64 (64-bit)..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
|
||||||
|
else
|
||||||
|
# Fallback auf v1.29.2 für unbekannte ARM-Architekturen
|
||||||
|
log "Verwende automatische Architekturerkennung für Docker Compose v1.29.2..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Für andere Systeme versuche zuerst v2, dann v1.29.2 als Fallback
|
||||||
|
log "Installiere Docker Compose v2 für $(uname -s)/$(uname -m)..."
|
||||||
|
if ! sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose; then
|
||||||
|
log "${YELLOW}Konnte Docker Compose v2 nicht herunterladen. Versuche v1.29.2...${NC}"
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Konnte Docker Compose nicht herunterladen. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
log "${GREEN}Docker Compose wurde installiert.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Starte Docker-Dienst
|
||||||
|
if command -v systemctl &> /dev/null; then
|
||||||
|
sudo systemctl enable docker
|
||||||
|
sudo systemctl start docker
|
||||||
|
elif command -v service &> /dev/null; then
|
||||||
|
sudo service docker enable
|
||||||
|
sudo service docker start
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfen ob Docker installiert ist
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
log "${YELLOW}Docker ist nicht installiert.${NC}"
|
||||||
|
read -p "Möchten Sie Docker installieren? (j/n): " install_docker_choice
|
||||||
|
if [[ "$install_docker_choice" == "j" ]]; then
|
||||||
|
install_docker
|
||||||
|
else
|
||||||
|
error_log "Docker wird für die Installation benötigt. Bitte installieren Sie Docker manuell."
|
||||||
|
log "Siehe: https://docs.docker.com/get-docker/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen ob Docker Daemon läuft
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
log "${YELLOW}Docker-Daemon läuft nicht. Versuche, den Dienst zu starten...${NC}"
|
||||||
|
|
||||||
|
# Versuche, Docker zu starten
|
||||||
|
if command -v systemctl &> /dev/null; then
|
||||||
|
sudo systemctl start docker
|
||||||
|
elif command -v service &> /dev/null; then
|
||||||
|
sudo service docker start
|
||||||
|
else
|
||||||
|
error_log "Konnte Docker-Daemon nicht starten. Bitte starten Sie den Docker-Dienst manuell."
|
||||||
|
log "Starten mit: sudo systemctl start docker oder sudo service docker start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe erneut, ob Docker läuft
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
error_log "Docker-Daemon konnte nicht gestartet werden. Bitte starten Sie den Docker-Dienst manuell."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}Docker-Daemon wurde erfolgreich gestartet.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen ob Docker Compose installiert ist
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
log "${GREEN}Docker Compose v2 Plugin ist bereits installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=true
|
||||||
|
elif command -v docker-compose &> /dev/null; then
|
||||||
|
log "${GREEN}Docker Compose v1 ist bereits installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=false
|
||||||
|
else
|
||||||
|
log "${YELLOW}Docker Compose ist nicht installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=false
|
||||||
|
read -p "Möchten Sie Docker Compose installieren? (j/n): " install_compose_choice
|
||||||
|
if [[ "$install_compose_choice" == "j" ]]; then
|
||||||
|
log "${YELLOW}Installiere Docker Compose...${NC}"
|
||||||
|
|
||||||
|
# Prüfe ob das Betriebssystem ARM-basiert ist (z.B. Raspberry Pi)
|
||||||
|
if grep -q "arm" /proc/cpuinfo 2> /dev/null; then
|
||||||
|
ARCH=$(dpkg --print-architecture 2> /dev/null || echo "unknown")
|
||||||
|
IS_RASPBERRY_PI=true
|
||||||
|
else
|
||||||
|
IS_RASPBERRY_PI=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Versuche zuerst Docker Compose v2 zu installieren
|
||||||
|
if [ "$IS_RASPBERRY_PI" = true ]; then
|
||||||
|
if [ "$ARCH" = "armhf" ]; then
|
||||||
|
log "Installiere Docker Compose für armhf (32-bit)..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose
|
||||||
|
elif [ "$ARCH" = "arm64" ]; then
|
||||||
|
log "Installiere Docker Compose für arm64 (64-bit)..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
|
||||||
|
else
|
||||||
|
log "Verwende automatische Architekturerkennung für Docker Compose v1.29.2..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Installiere Docker Compose v2 für $(uname -s)/$(uname -m)..."
|
||||||
|
if ! sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose; then
|
||||||
|
log "${YELLOW}Konnte Docker Compose v2 nicht herunterladen. Versuche v1.29.2...${NC}"
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Konnte Docker Compose nicht herunterladen. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
log "${GREEN}Docker Compose wurde installiert.${NC}"
|
||||||
|
else
|
||||||
|
error_log "Docker Compose wird für die Installation benötigt. Bitte installieren Sie es manuell."
|
||||||
|
log "Siehe: https://docs.docker.com/compose/install/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen ob wget installiert ist (wird für healthcheck verwendet)
|
||||||
|
if ! command -v wget &> /dev/null; then
|
||||||
|
error_log "wget ist nicht installiert, wird aber für den Container-Healthcheck benötigt."
|
||||||
|
log "Installation mit: sudo apt-get install wget"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wechsle ins Backend-Verzeichnis
|
||||||
|
log "Wechsle ins Verzeichnis: $BACKEND_DIR"
|
||||||
|
cd "$BACKEND_DIR" || {
|
||||||
|
error_log "Konnte nicht ins Verzeichnis $BACKEND_DIR wechseln."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfe ob Dockerfile existiert
|
||||||
|
if [ ! -f "Dockerfile" ]; then
|
||||||
|
error_log "Dockerfile nicht gefunden in $BACKEND_DIR."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe ob docker-compose.yml existiert
|
||||||
|
if [ ! -f "docker-compose.yml" ]; then
|
||||||
|
error_log "docker-compose.yml nicht gefunden in $BACKEND_DIR."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle .env-Datei
|
||||||
|
log "${YELLOW}Erstelle .env Datei...${NC}"
|
||||||
|
cat > .env << EOL
|
||||||
|
SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
||||||
|
DATABASE_PATH=instance/myp.db
|
||||||
|
TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
||||||
|
TAPO_PASSWORD=744563017196A
|
||||||
|
PRINTERS={"Printer 1": {"ip": "192.168.0.100"}, "Printer 2": {"ip": "192.168.0.101"}, "Printer 3": {"ip": "192.168.0.102"}, "Printer 4": {"ip": "192.168.0.103"}, "Printer 5": {"ip": "192.168.0.104"}, "Printer 6": {"ip": "192.168.0.106"}}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
error_log "Konnte .env-Datei nicht erstellen. Prüfen Sie die Berechtigungen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "${GREEN}.env Datei erfolgreich erstellt${NC}"
|
||||||
|
|
||||||
|
# Verzeichnisse erstellen
|
||||||
|
log "Erstelle benötigte Verzeichnisse"
|
||||||
|
if ! mkdir -p logs; then
|
||||||
|
error_log "Konnte Verzeichnis 'logs' nicht erstellen. Prüfen Sie die Berechtigungen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! mkdir -p instance; then
|
||||||
|
error_log "Konnte Verzeichnis 'instance' nicht erstellen. Prüfen Sie die Berechtigungen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker-Image bauen und starten
|
||||||
|
log "${YELLOW}Baue und starte Backend-Container...${NC}"
|
||||||
|
log "${YELLOW}Dies kann auf einem Raspberry Pi einige Minuten dauern - bitte geduldig sein${NC}"
|
||||||
|
|
||||||
|
# Prüfe, ob Docker-Daemon läuft
|
||||||
|
if ! docker info &>/dev/null; then
|
||||||
|
log "${YELLOW}Docker-Daemon scheint nicht zu laufen. Versuche zu starten...${NC}"
|
||||||
|
|
||||||
|
# Versuche Docker zu starten
|
||||||
|
if command -v systemctl &>/dev/null; then
|
||||||
|
sudo systemctl start docker || true
|
||||||
|
sleep 5
|
||||||
|
elif command -v service &>/dev/null; then
|
||||||
|
sudo service docker start || true
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe erneut, ob Docker jetzt läuft
|
||||||
|
if ! docker info &>/dev/null; then
|
||||||
|
error_log "Docker-Daemon konnte nicht gestartet werden."
|
||||||
|
log "Führen Sie vor der Installation bitte folgende Befehle aus:"
|
||||||
|
log " sudo systemctl start docker"
|
||||||
|
log " sudo systemctl enable docker"
|
||||||
|
log "Starten Sie dann das Installationsskript erneut."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker-Rechte prüfen
|
||||||
|
if ! docker ps &>/dev/null; then
|
||||||
|
error_log "Sie haben keine Berechtigung, Docker ohne sudo zu verwenden."
|
||||||
|
log "Bitte führen Sie folgenden Befehl aus und melden Sie sich danach neu an:"
|
||||||
|
log " sudo usermod -aG docker $USER"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob erforderliche Basis-Images lokal verfügbar sind
|
||||||
|
if ! docker image inspect python:3-slim &>/dev/null; then
|
||||||
|
log "${YELLOW}Prüfe und setze DNS-Server für Docker...${NC}"
|
||||||
|
|
||||||
|
# DNS-Einstellungen prüfen und anpassen
|
||||||
|
if [ -f /etc/docker/daemon.json ]; then
|
||||||
|
log "Bestehende Docker-Konfiguration gefunden."
|
||||||
|
else
|
||||||
|
log "Erstelle Docker-Konfiguration mit Google DNS..."
|
||||||
|
sudo mkdir -p /etc/docker
|
||||||
|
echo '{
|
||||||
|
"dns": ["8.8.8.8", "8.8.4.4"]
|
||||||
|
}' | sudo tee /etc/docker/daemon.json > /dev/null
|
||||||
|
|
||||||
|
# Docker neu starten, damit die Änderungen wirksam werden
|
||||||
|
if command -v systemctl &>/dev/null; then
|
||||||
|
sudo systemctl restart docker
|
||||||
|
sleep 5
|
||||||
|
elif command -v service &>/dev/null; then
|
||||||
|
sudo service docker restart
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Versuche Image explizit mit anderen Tags herunterzuladen
|
||||||
|
log "${YELLOW}Versuche lokal vorhandene Python-Version zu finden...${NC}"
|
||||||
|
|
||||||
|
# Suche nach allen verfügbaren Python-Images
|
||||||
|
PYTHON_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "python:")
|
||||||
|
|
||||||
|
if [ -n "$PYTHON_IMAGES" ]; then
|
||||||
|
log "Gefundene Python-Images: $PYTHON_IMAGES"
|
||||||
|
# Verwende das erste gefundene Python-Image
|
||||||
|
FIRST_PYTHON=$(echo "$PYTHON_IMAGES" | head -n 1)
|
||||||
|
log "${GREEN}Verwende vorhandenes Python-Image: $FIRST_PYTHON${NC}"
|
||||||
|
|
||||||
|
# Aktualisiere den Dockerfile
|
||||||
|
sed -i "s|FROM python:3-slim|FROM $FIRST_PYTHON|g" Dockerfile
|
||||||
|
log "Dockerfile aktualisiert, um lokales Image zu verwenden."
|
||||||
|
else
|
||||||
|
# Versuche unterschiedliche Python-Versionen
|
||||||
|
for PYTHON_VERSION in "python:3.11-slim" "python:3.10-slim" "python:3.9-slim" "python:slim" "python:alpine"; do
|
||||||
|
log "Versuche $PYTHON_VERSION zu laden..."
|
||||||
|
if docker pull $PYTHON_VERSION; then
|
||||||
|
log "${GREEN}Erfolgreich $PYTHON_VERSION heruntergeladen${NC}"
|
||||||
|
# Aktualisiere den Dockerfile
|
||||||
|
sed -i "s|FROM python:3-slim|FROM $PYTHON_VERSION|g" Dockerfile
|
||||||
|
log "Dockerfile aktualisiert, um $PYTHON_VERSION zu verwenden."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erhöhe Docker-Timeout für langsame Verbindungen und Raspberry Pi
|
||||||
|
export DOCKER_CLIENT_TIMEOUT=300
|
||||||
|
export COMPOSE_HTTP_TIMEOUT=300
|
||||||
|
|
||||||
|
# Verwende die richtige Docker Compose Version
|
||||||
|
if [ "${DOCKER_COMPOSE_V2:-false}" = true ]; then
|
||||||
|
# Docker Compose V2 Plugin (docker compose)
|
||||||
|
log "Baue lokales Image..."
|
||||||
|
if ! docker compose build --no-cache; then
|
||||||
|
error_log "Docker Compose Build (v2) fehlgeschlagen. Versuche mit v1 Format..."
|
||||||
|
if ! docker-compose build --no-cache; then
|
||||||
|
error_log "Docker Compose Build fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starte Container aus lokalem Image..."
|
||||||
|
if ! docker compose up -d; then
|
||||||
|
error_log "Docker Compose Up (v2) fehlgeschlagen. Versuche mit v1 Format..."
|
||||||
|
if ! docker-compose up -d; then
|
||||||
|
error_log "Docker Compose Up fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Docker Compose V1 (docker-compose)
|
||||||
|
log "Baue lokales Image..."
|
||||||
|
if ! docker-compose build --no-cache; then
|
||||||
|
error_log "Docker Compose Build fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starte Container aus lokalem Image..."
|
||||||
|
if ! docker-compose up -d; then
|
||||||
|
error_log "Docker Compose Up fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob der Container läuft
|
||||||
|
log "Warte 10 Sekunden, bis der Container gestartet ist..."
|
||||||
|
sleep 10
|
||||||
|
if docker ps | grep -q "myp-backend"; then
|
||||||
|
log "${GREEN}Backend-Container läuft${NC}"
|
||||||
|
else
|
||||||
|
error_log "Backend-Container läuft nicht. Container-Status:"
|
||||||
|
docker ps -a | grep myp-backend
|
||||||
|
log "Container-Logs:"
|
||||||
|
docker logs myp-backend
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test API-Endpunkt
|
||||||
|
log "${YELLOW}Teste Backend-API...${NC}"
|
||||||
|
log "${YELLOW}HINWEIS: Der API-Server ist bei der ersten Installation oft noch nicht erreichbar${NC}"
|
||||||
|
log "${YELLOW}Dies ist ein bekanntes Verhalten wegen der Netzwerkkonfiguration${NC}"
|
||||||
|
log "${YELLOW}Bitte nach der Installation das System neu starten, danach sollte der API-Server erreichbar sein${NC}"
|
||||||
|
|
||||||
|
# Wir versuchen es trotzdem einmal, um zu sehen, ob er vielleicht doch läuft
|
||||||
|
if curl -s http://localhost:5000/health 2>/dev/null | grep -q "healthy"; then
|
||||||
|
log "${GREEN}Backend-API ist erreichbar und funktioniert${NC}"
|
||||||
|
else
|
||||||
|
log "${YELLOW}Backend-API ist wie erwartet noch nicht erreichbar${NC}"
|
||||||
|
log "${GREEN}Das ist völlig normal bei der Erstinstallation${NC}"
|
||||||
|
log "${GREEN}Nach einem Neustart des Systems sollte der API-Server korrekt erreichbar sein${NC}"
|
||||||
|
log "Container-Status prüfen mit: docker logs myp-backend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialisierung der Datenbank prüfen
|
||||||
|
log "${YELLOW}Prüfe Datenbank-Initialisierung...${NC}"
|
||||||
|
if [ ! -s "instance/myp.db" ]; then
|
||||||
|
log "${YELLOW}Datenbank scheint leer zu sein. Führe Initialisierungsskript aus...${NC}"
|
||||||
|
DB_INIT_OUTPUT=$(docker exec myp-backend python -c "from app import init_db; init_db()" 2>&1)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "${GREEN}Datenbank erfolgreich initialisiert${NC}"
|
||||||
|
else
|
||||||
|
error_log "Fehler bei der Datenbank-Initialisierung:"
|
||||||
|
echo "$DB_INIT_OUTPUT"
|
||||||
|
log "Container-Logs:"
|
||||||
|
docker logs myp-backend
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "${GREEN}Datenbank existiert bereits${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Teste, ob ein API-Endpunkt Daten zurückgibt
|
||||||
|
log "${YELLOW}Teste Datenbank-Verbindung über API...${NC}"
|
||||||
|
if curl -s http://localhost:5000/api/printers | grep -q "\[\]"; then
|
||||||
|
log "${GREEN}Datenbank-Verbindung funktioniert${NC}"
|
||||||
|
else
|
||||||
|
log "${YELLOW}API gibt keine leere Drucker-Liste zurück. Möglicherweise ist die DB nicht korrekt initialisiert.${NC}"
|
||||||
|
log "API-Antwort:"
|
||||||
|
curl -s http://localhost:5000/api/printers
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}=== Installation abgeschlossen ===${NC}"
|
||||||
|
log "${YELLOW}WICHTIG: Nach der Erstinstallation ist ein Systemneustart erforderlich${NC}"
|
||||||
|
log "${YELLOW}Danach ist das Backend unter http://localhost:5000 erreichbar${NC}"
|
||||||
|
log "Anzeigen der Logs: docker logs -f myp-backend"
|
||||||
|
|
||||||
|
# Verwende die richtige Docker Compose Version für Hinweis
|
||||||
|
if [ "${DOCKER_COMPOSE_V2:-false}" = true ]; then
|
||||||
|
log "Backend stoppen: docker compose -f $BACKEND_DIR/docker-compose.yml down"
|
||||||
|
else
|
||||||
|
log "Backend stoppen: docker-compose -f $BACKEND_DIR/docker-compose.yml down"
|
||||||
|
fi
|
@ -1 +0,0 @@
|
|||||||
PRINTER_IPS=192.168.0.10,192.168.0.11,192.168.0.12
|
|
@ -1,106 +0,0 @@
|
|||||||
# 🖨️ 3D-Drucker Status API 📊
|
|
||||||
|
|
||||||
Willkommen beim Blueprint der 3D-Drucker Status API! Diese API ermöglicht es Ihnen, den Status mehrerer über LAN verbundener 3D-Drucker zu überwachen und Druckaufträge an sie zu senden.
|
|
||||||
|
|
||||||
## 🌟 Funktionen
|
|
||||||
|
|
||||||
- 🔍 Abrufen des Status von 3D-Druckern, einschließlich ihres aktuellen Status, Fortschrittes und Temperatur.
|
|
||||||
- 📥 Senden von Druckaufträgen an verfügbare 3D-Drucker.
|
|
||||||
- 💾 Speichern und Aktualisieren des Status jedes Druckers in einer SQLite-Datenbank.
|
|
||||||
|
|
||||||
## 🛠️Verwendete Technologien
|
|
||||||
|
|
||||||
- 🐍 Python
|
|
||||||
- 🌶️ Flask
|
|
||||||
- 🗄️ SQLite
|
|
||||||
- 🌐 HTTP-Anfragen
|
|
||||||
|
|
||||||
## 📋 Verordnungen
|
|
||||||
|
|
||||||
Bevor Sie die API starten, stellen Sie sicher, dass Sie folgendes haben:
|
|
||||||
|
|
||||||
- Python 3.x installiert
|
|
||||||
- Flask und python-dotenv-Bibliotheken installiert (`pip install flask python-dotenv`)
|
|
||||||
- Eine Liste von IP-Adressen der 3D-Drucker, die Sie überwachen möchten
|
|
||||||
|
|
||||||
## 🚀 Erste Schritte
|
|
||||||
|
|
||||||
1. Klonen Sie das Repository:
|
|
||||||
```
|
|
||||||
git clone https://git.i.mercedes-benz.com/TBA-Berlin-FI/MYP
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Installieren Sie die erforderlichen Abhängigkeiten:
|
|
||||||
```
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Erstellen Sie eine `.env`-Datei im Projektverzeichnis und geben Sie die IP-Adressen Ihrer 3D-Drucker an:
|
|
||||||
```
|
|
||||||
PRINTER_IPS=192.168.0.10,192.168.0.11,192.168.0.12
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Starten Sie das Skript, um die SQLite-Datenbank zu erstellen:
|
|
||||||
```
|
|
||||||
python create_db.py
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Starten Sie den API-Server:
|
|
||||||
```
|
|
||||||
python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Die API ist unter `http://localhost:5000` erreichbar.
|
|
||||||
|
|
||||||
## 📡 API-Endpunkte
|
|
||||||
|
|
||||||
- `GET /printer_status`: Rufen Sie den Status aller 3D-Drucker ab.
|
|
||||||
- `POST /print_job`: Senden Sie einen Druckauftrag an einen bestimmten 3D-Drucker.
|
|
||||||
|
|
||||||
## 📝 API-Nutzung
|
|
||||||
|
|
||||||
### Druckerstatus abrufen
|
|
||||||
|
|
||||||
Senden Sie eine `GET`-Anfrage an `/printer_status`, um den Status aller 3D-Drucker abzurufen.
|
|
||||||
|
|
||||||
Antwort:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"ip": "192.168.0.10",
|
|
||||||
"status": "frei",
|
|
||||||
"progress": 0,
|
|
||||||
"temperature": 25
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ip": "192.168.0.11",
|
|
||||||
"status": "besetzt",
|
|
||||||
"progress": 50,
|
|
||||||
"temperature": 180
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag senden
|
|
||||||
|
|
||||||
Senden Sie eine `POST`-Anfrage an `/print_job` mit der folgenden JSON-Last, um einen Druckauftrag an einen bestimmten 3D-Drucker zu senden:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"printer_ip": "192.168.0.10",
|
|
||||||
"file_url": "http://example.com/print_file.gcode"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Antwort:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Druckauftrag gestartet"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 📄 Lizenz
|
|
||||||
|
|
||||||
- --> Noch nicht verfügbar
|
|
@ -1,25 +0,0 @@
|
|||||||
import sqlite3
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
printers = os.getenv('PRINTER_IPS').split(',')
|
|
||||||
|
|
||||||
def create_db():
|
|
||||||
conn = sqlite3.connect('printers.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
|
|
||||||
# Tabelle 'printers' erstellen, falls sie nicht existiert
|
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS printers
|
|
||||||
(ip TEXT PRIMARY KEY, status TEXT)''')
|
|
||||||
|
|
||||||
# Drucker-IPs in die Tabelle einfügen, falls sie noch nicht vorhanden sind
|
|
||||||
for printer_ip in printers:
|
|
||||||
c.execute("INSERT OR IGNORE INTO printers (ip, status) VALUES (?, ?)", (printer_ip, "frei"))
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
print("Datenbank 'printers.db' erfolgreich erstellt.")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
create_db()
|
|
@ -1,3 +0,0 @@
|
|||||||
flask==2.1.0
|
|
||||||
requests==2.25.1
|
|
||||||
python-dotenv==0.20.0
|
|
@ -1,94 +0,0 @@
|
|||||||
from flask import Flask, jsonify, request
|
|
||||||
import requests
|
|
||||||
import sqlite3
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
printers = os.getenv('PRINTER_IPS').split(',')
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# SQLite-Datenbank initialisieren
|
|
||||||
def init_db():
|
|
||||||
conn = sqlite3.connect('printers.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS printers
|
|
||||||
(ip TEXT PRIMARY KEY, status TEXT)''')
|
|
||||||
for printer_ip in printers:
|
|
||||||
c.execute("INSERT OR IGNORE INTO printers (ip, status) VALUES (?, ?)", (printer_ip, "frei"))
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
@app.route('/printer_status', methods=['GET'])
|
|
||||||
def get_printer_status():
|
|
||||||
printer_status = []
|
|
||||||
conn = sqlite3.connect('printers.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
|
|
||||||
for printer_ip in printers:
|
|
||||||
c.execute("SELECT status FROM printers WHERE ip = ?", (printer_ip,))
|
|
||||||
status = c.fetchone()[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(f"http://{printer_ip}/api/printer/status")
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
status_data = response.json()
|
|
||||||
printer_status.append({
|
|
||||||
"ip": printer_ip,
|
|
||||||
"status": status,
|
|
||||||
"progress": status_data["progress"],
|
|
||||||
"temperature": status_data["temperature"]
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
printer_status.append({
|
|
||||||
"ip": printer_ip,
|
|
||||||
"status": "Fehler bei der Abfrage",
|
|
||||||
"progress": None,
|
|
||||||
"temperature": None
|
|
||||||
})
|
|
||||||
except:
|
|
||||||
printer_status.append({
|
|
||||||
"ip": printer_ip,
|
|
||||||
"status": "Drucker nicht erreichbar",
|
|
||||||
"progress": None,
|
|
||||||
"temperature": None
|
|
||||||
})
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return jsonify(printer_status)
|
|
||||||
|
|
||||||
@app.route('/print_job', methods=['POST'])
|
|
||||||
def submit_print_job():
|
|
||||||
print_job = request.json
|
|
||||||
printer_ip = print_job["printer_ip"]
|
|
||||||
file_url = print_job["file_url"]
|
|
||||||
|
|
||||||
conn = sqlite3.connect('printers.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute("SELECT status FROM printers WHERE ip = ?", (printer_ip,))
|
|
||||||
status = c.fetchone()[0]
|
|
||||||
|
|
||||||
if status == "frei":
|
|
||||||
try:
|
|
||||||
response = requests.post(f"http://{printer_ip}/api/print_job", json={"file_url": file_url})
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
c.execute("UPDATE printers SET status = 'besetzt' WHERE ip = ?", (printer_ip,))
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"message": "Druckauftrag gestartet"}), 200
|
|
||||||
else:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"message": "Fehler beim Starten des Druckauftrags"}), 500
|
|
||||||
except:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"message": "Drucker nicht erreichbar"}), 500
|
|
||||||
else:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"message": "Drucker ist nicht frei"}), 400
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
init_db()
|
|
||||||
app.run(host='0.0.0.0', port=5000)
|
|
@ -1,38 +0,0 @@
|
|||||||
# entwendet aus:
|
|
||||||
# https://github.com/ut-hnl-lab/ultimakerpy
|
|
||||||
|
|
||||||
# auch zum lesen:
|
|
||||||
# https://github.com/MartinBienz/SDPremote?tab=readme-ov-file
|
|
||||||
|
|
||||||
import time
|
|
||||||
from ultimakerpy import UMS3, JobState
|
|
||||||
|
|
||||||
def print_started(state):
|
|
||||||
if state == JobState.PRINTING:
|
|
||||||
time.sleep(6.0)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def layer_reached(pos, n):
|
|
||||||
if round(pos / 0.2) >= n: # set layer pitch: 0.2 mm
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
printer = UMS3(name='MyPrinterName')
|
|
||||||
targets = {
|
|
||||||
'job_state': printer.job_state,
|
|
||||||
'bed_pos': printer.bed.position,
|
|
||||||
}
|
|
||||||
|
|
||||||
printer.print_from_dialog() # select file to print
|
|
||||||
printer.peripherals.camera_streaming()
|
|
||||||
with printer.data_logger('output2.csv', targets) as dl:
|
|
||||||
timer = dl.get_timer()
|
|
||||||
|
|
||||||
# sleep until active leveling finishes
|
|
||||||
timer.wait_for_datalog('job_state', print_started)
|
|
||||||
|
|
||||||
for n in range(1, 101):
|
|
||||||
# sleep until the printing of specified layer to start
|
|
||||||
timer.wait_for_datalog('bed_pos', lambda v: layer_reached(v, n))
|
|
||||||
print('printing layer:', n)
|
|
@ -1,148 +0,0 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, session
|
|
||||||
import sqlite3
|
|
||||||
import bcrypt
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = 'supersecretkey'
|
|
||||||
|
|
||||||
# Database setup
|
|
||||||
def init_db():
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)''')
|
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS printers (id INTEGER PRIMARY KEY, name TEXT, status TEXT)''')
|
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS jobs (id INTEGER PRIMARY KEY, printer_id INTEGER, user TEXT, date TEXT, status TEXT)''')
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
init_db()
|
|
||||||
|
|
||||||
# User registration (Admin setup)
|
|
||||||
def add_admin():
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
hashed_pw = bcrypt.hashpw('adminpassword'.encode('utf-8'), bcrypt.gensalt())
|
|
||||||
c.execute("INSERT INTO users (username, password) VALUES (?, ?)", ('admin', hashed_pw))
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# Comment the next line after the first run
|
|
||||||
# add_admin()
|
|
||||||
|
|
||||||
# API Endpoints
|
|
||||||
@app.route('/api/printers/status', methods=['GET'])
|
|
||||||
def get_printer_status():
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute("SELECT * FROM printers")
|
|
||||||
printers = c.fetchall()
|
|
||||||
conn.close()
|
|
||||||
return jsonify(printers)
|
|
||||||
|
|
||||||
@app.route('/api/printers/job', methods=['POST'])
|
|
||||||
def create_job():
|
|
||||||
if not session.get('logged_in'):
|
|
||||||
return jsonify({'error': 'Unauthorized'}), 403
|
|
||||||
|
|
||||||
data = request.json
|
|
||||||
user = session['username']
|
|
||||||
printer_id = data['printer_id']
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
|
|
||||||
c.execute("SELECT status FROM printers WHERE id=?", (printer_id,))
|
|
||||||
status = c.fetchone()[0]
|
|
||||||
|
|
||||||
if status == 'frei':
|
|
||||||
c.execute("INSERT INTO jobs (printer_id, user, date, status) VALUES (?, ?, datetime('now'), 'in progress')",
|
|
||||||
(printer_id, user))
|
|
||||||
c.execute("UPDATE printers SET status='belegt' WHERE id=?", (printer_id,))
|
|
||||||
conn.commit()
|
|
||||||
elif status == 'belegt':
|
|
||||||
return jsonify({'error': 'Printer already in use'}), 409
|
|
||||||
else:
|
|
||||||
return jsonify({'error': 'Invalid printer status'}), 400
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return jsonify({'message': 'Job created and printer turned on'}), 200
|
|
||||||
|
|
||||||
@app.route('/api/printers/reserve', methods=['POST'])
|
|
||||||
def reserve_printer():
|
|
||||||
if not session.get('logged_in'):
|
|
||||||
return jsonify({'error': 'Unauthorized'}), 403
|
|
||||||
|
|
||||||
data = request.json
|
|
||||||
printer_id = data['printer_id']
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
|
|
||||||
c.execute("SELECT status FROM printers WHERE id=?", (printer_id,))
|
|
||||||
status = c.fetchone()[0]
|
|
||||||
|
|
||||||
if status == 'frei':
|
|
||||||
c.execute("UPDATE printers SET status='reserviert' WHERE id=?", (printer_id,))
|
|
||||||
conn.commit()
|
|
||||||
message = 'Printer reserved'
|
|
||||||
else:
|
|
||||||
message = 'Printer cannot be reserved'
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return jsonify({'message': message}), 200
|
|
||||||
|
|
||||||
@app.route('/api/printers/release', methods=['POST'])
|
|
||||||
def release_printer():
|
|
||||||
if not session.get('logged_in'):
|
|
||||||
return jsonify({'error': 'Unauthorized'}), 403
|
|
||||||
|
|
||||||
data = request.json
|
|
||||||
printer_id = data['printer_id']
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
|
|
||||||
c.execute("UPDATE printers SET status='frei' WHERE id=?", (printer_id,))
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
return jsonify({'message': 'Printer released'}), 200
|
|
||||||
|
|
||||||
# Authentication routes
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
|
||||||
def login():
|
|
||||||
if request.method == 'POST':
|
|
||||||
username = request.form['username']
|
|
||||||
password = request.form['password'].encode('utf-8')
|
|
||||||
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute("SELECT * FROM users WHERE username=?", (username,))
|
|
||||||
user = c.fetchone()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if user and bcrypt.checkpw(password, user[2].encode('utf-8')):
|
|
||||||
session['logged_in'] = True
|
|
||||||
session['username'] = username
|
|
||||||
return redirect(url_for('dashboard'))
|
|
||||||
else:
|
|
||||||
return render_template('login.html', error='Invalid Credentials')
|
|
||||||
|
|
||||||
return render_template('login.html')
|
|
||||||
|
|
||||||
@app.route('/dashboard')
|
|
||||||
def dashboard():
|
|
||||||
if not session.get('logged_in'):
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
conn = sqlite3.connect('database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute("SELECT * FROM printers")
|
|
||||||
printers = c.fetchall()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return render_template('dashboard.html', printers=printers)
|
|
||||||
|
|
||||||
@app.route('/logout')
|
|
||||||
def logout():
|
|
||||||
session.clear()
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(debug=True)
|
|
@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>3D Printer Management</title>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@1.14.0/dist/full.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body class="bg-black text-white">
|
|
||||||
<nav class="bg-gray-800 p-4">
|
|
||||||
<div class="container mx-auto">
|
|
||||||
<h1 class="text-xl">3D Printer Management Dashboard</h1>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div class="container mx-auto mt-5">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,29 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h2 class="text-2xl mb-4">Printer Status</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{% for printer in printers %}
|
|
||||||
<div class="card bg-gray-900 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">{{ printer[1] }}</h2>
|
|
||||||
<p>Status: {{ printer[2] }}</p>
|
|
||||||
{% if printer[2] == 'frei' %}
|
|
||||||
<form method="POST" action="/api/printers/job">
|
|
||||||
<input type="hidden" name="printer_id" value="{{ printer[0] }}">
|
|
||||||
<button class="btn btn-success mt-4 w-full">Start Job</button>
|
|
||||||
</form>
|
|
||||||
{% elif printer[2] == 'belegt' %}
|
|
||||||
<button class="btn btn-warning mt-4 w-full" disabled>In Use</button>
|
|
||||||
{% elif printer[2] == 'reserviert' %}
|
|
||||||
<form method="POST" action="/api/printers/release">
|
|
||||||
<input type="hidden" name="printer_id" value="{{ printer[0] }}">
|
|
||||||
<button class="btn btn-info mt-4 w-full">Release</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<a href="/logout" class="btn btn-secondary mt-4">Logout</a>
|
|
||||||
{% endblock %}
|
|
@ -1,33 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="flex justify-center items-center h-screen">
|
|
||||||
<div class="card w-96 bg-gray-900 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Login</h2>
|
|
||||||
<form method="POST">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Username</span>
|
|
||||||
</label>
|
|
||||||
<input type="text" name="username" class="input input-bordered w-full" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Password</span>
|
|
||||||
</label>
|
|
||||||
<input type="password" name="password" class="input input-bordered w-full" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-control mt-6">
|
|
||||||
<button class="btn btn-primary w-full">Login</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% if error %}
|
|
||||||
<div class="mt-4 text-red-500">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -12,7 +12,7 @@ node_modules
|
|||||||
db/
|
db/
|
||||||
|
|
||||||
# Ignore local configuration files
|
# Ignore local configuration files
|
||||||
.env
|
#.env
|
||||||
.env.example
|
.env.example
|
||||||
|
|
||||||
# Ignore version control files
|
# Ignore version control files
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
# OAuth Configuration
|
|
||||||
OAUTH_CLIENT_ID=client_id
|
|
||||||
OAUTH_CLIENT_SECRET=client_secret
|
|
0
packages/reservation-platform/docker/build.sh
Executable file → Normal file
0
packages/reservation-platform/docker/build.sh
Executable file → Normal file
@ -2,7 +2,36 @@
|
|||||||
debug
|
debug
|
||||||
}
|
}
|
||||||
|
|
||||||
m040tbaraspi001.de040.corpintra.net, m040tbaraspi001.de040.corpinter.net {
|
# Hauptdomain für die Anwendung
|
||||||
|
m040tbaraspi001.de040.corpintra.net, m040tbaraspi001, localhost {
|
||||||
reverse_proxy myp-rp:3000
|
reverse_proxy myp-rp:3000
|
||||||
tls internal
|
tls internal
|
||||||
|
|
||||||
|
# Erlaube HTTP -> HTTPS Redirects für OAuth
|
||||||
|
@oauth path /auth/login/callback*
|
||||||
|
handle @oauth {
|
||||||
|
header Cache-Control "no-cache"
|
||||||
|
reverse_proxy myp-rp:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allgemeine Header für Sicherheit und Caching
|
||||||
|
header {
|
||||||
|
# Sicherheitsheader
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Cache-Control für statische Assets
|
||||||
|
@static {
|
||||||
|
path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
||||||
|
}
|
||||||
|
header @static Cache-Control "public, max-age=86400"
|
||||||
|
|
||||||
|
# Keine Caches für dynamische Inhalte
|
||||||
|
@dynamic {
|
||||||
|
not path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
||||||
|
}
|
||||||
|
header @dynamic Cache-Control "no-store, no-cache, must-revalidate"
|
||||||
|
}
|
||||||
}
|
}
|
@ -13,7 +13,18 @@ services:
|
|||||||
myp-rp:
|
myp-rp:
|
||||||
image: myp-rp:latest
|
image: myp-rp:latest
|
||||||
container_name: myp-rp
|
container_name: myp-rp
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||||
|
- OAUTH_CLIENT_ID=client_id
|
||||||
|
- OAUTH_CLIENT_SECRET=client_secret
|
||||||
env_file: "/srv/myp-env/github.env"
|
env_file: "/srv/myp-env/github.env"
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/MYP-DB:/usr/src/app/db
|
- /srv/MYP-DB:/usr/src/app/db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# Füge Healthcheck hinzu für besseres Monitoring
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
0
packages/reservation-platform/docker/save.sh
Executable file → Normal file
0
packages/reservation-platform/docker/save.sh
Executable file → Normal file
@ -5,7 +5,7 @@
|
|||||||
"packageManager": "pnpm@9.12.1",
|
"packageManager": "pnpm@9.12.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "node update-package.js && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"db:create-default": "mkdir -p db/",
|
"db:create-default": "mkdir -p db/",
|
||||||
|
82
packages/reservation-platform/setup-backend-url.sh
Executable file
82
packages/reservation-platform/setup-backend-url.sh
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Skript zum Setzen der Backend-URL in der Frontend-Konfiguration
|
||||||
|
# Verwendet für die Verbindung zum Backend-Server unter 192.168.0.105:5000
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktion zur Ausgabe mit Zeitstempel
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Definiere Variablen
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
ENV_FILE="$SCRIPT_DIR/.env.local"
|
||||||
|
DEFAULT_BACKEND_URL="http://192.168.0.105:5000"
|
||||||
|
|
||||||
|
# Falls übergebene Parameter vorhanden sind, Backend-URL anpassen
|
||||||
|
if [ -n "$1" ]; then
|
||||||
|
BACKEND_URL="$1"
|
||||||
|
log "Verwende übergebene Backend-URL: ${BACKEND_URL}"
|
||||||
|
else
|
||||||
|
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
||||||
|
log "Verwende Standard-Backend-URL: ${BACKEND_URL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bestimme den Hostnamen für OAuth
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
||||||
|
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||||
|
OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
|
||||||
|
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
else
|
||||||
|
FRONTEND_HOSTNAME="$HOSTNAME"
|
||||||
|
OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback"
|
||||||
|
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle .env.local Datei mit Backend-URL
|
||||||
|
log "${YELLOW}Erstelle .env.local Datei...${NC}"
|
||||||
|
cat > "$ENV_FILE" << EOL
|
||||||
|
# Backend API Konfiguration
|
||||||
|
NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
||||||
|
|
||||||
|
# Frontend-URL für OAuth Callback
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
||||||
|
|
||||||
|
# Explizite OAuth Callback URL für GitHub
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
||||||
|
|
||||||
|
# OAuth Konfiguration (falls nötig)
|
||||||
|
OAUTH_CLIENT_ID=client_id
|
||||||
|
OAUTH_CLIENT_SECRET=client_secret
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Überprüfe, ob die Datei erstellt wurde
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
log "${GREEN}Erfolgreich .env.local Datei mit Backend-URL erstellt: ${BACKEND_URL}${NC}"
|
||||||
|
else
|
||||||
|
error_log "Konnte .env.local Datei nicht erstellen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Hinweis für Docker-Installation
|
||||||
|
log "${YELLOW}WICHTIG: Wenn Sie Docker verwenden, stellen Sie sicher, dass Sie die Umgebungsvariable setzen:${NC}"
|
||||||
|
log "NEXT_PUBLIC_API_URL=${BACKEND_URL}"
|
||||||
|
log ""
|
||||||
|
log "${GREEN}Backend-URL wurde erfolgreich konfiguriert. Nach einem Neustart der Anwendung sollte die Verbindung hergestellt werden.${NC}"
|
||||||
|
|
||||||
|
# Berechtigungen setzen
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
|
exit 0
|
99
packages/reservation-platform/src/app/api/jobs/[id]/route.ts
Normal file
99
packages/reservation-platform/src/app/api/jobs/[id]/route.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { API_ENDPOINTS } from "@/utils/api-config";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = params.id;
|
||||||
|
|
||||||
|
// Rufe einzelnen Job vom externen Backend ab
|
||||||
|
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = await response.json();
|
||||||
|
return Response.json(job);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen des Jobs vom Backend:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = params.id;
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Sende Job-Aktualisierung an das externe Backend
|
||||||
|
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return Response.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Aktualisieren des Jobs:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren des Jobs' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = params.id;
|
||||||
|
|
||||||
|
// Sende Job-Löschung an das externe Backend
|
||||||
|
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return Response.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Löschen des Jobs:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Löschen des Jobs' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
59
packages/reservation-platform/src/app/api/jobs/route.ts
Normal file
59
packages/reservation-platform/src/app/api/jobs/route.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { API_ENDPOINTS } from "@/utils/api-config";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Rufe Jobs vom externen Backend ab
|
||||||
|
const response = await fetch(API_ENDPOINTS.JOBS);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobs = await response.json();
|
||||||
|
return Response.json(jobs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Jobs vom Backend:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Sende Job-Erstellung an das externe Backend
|
||||||
|
const response = await fetch(API_ENDPOINTS.JOBS, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return Response.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen des Jobs:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Erstellen des Jobs' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,27 @@
|
|||||||
import { getPrinters } from "@/server/actions/printers";
|
import { API_ENDPOINTS } from "@/utils/api-config";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const printers = await getPrinters();
|
try {
|
||||||
|
// Rufe Drucker vom externen Backend ab statt von der lokalen Datenbank
|
||||||
|
const response = await fetch(API_ENDPOINTS.PRINTERS);
|
||||||
|
|
||||||
return Response.json(printers);
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const printers = await response.json();
|
||||||
|
return Response.json(printers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Drucker vom Backend:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { lucia } from "@/server/auth";
|
import { lucia } from "@/server/auth";
|
||||||
import { type GitHubUserResult, github } from "@/server/auth/oauth";
|
import { type GitHubUserResult, github, isValidCallbackHost, USED_CALLBACK_URL } from "@/server/auth/oauth";
|
||||||
|
import { ALLOWED_CALLBACK_HOSTS } from "@/utils/api-config";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { users } from "@/server/db/schema";
|
import { users } from "@/server/db/schema";
|
||||||
import { OAuth2RequestError } from "arctic";
|
import { OAuth2RequestError } from "arctic";
|
||||||
@ -21,11 +22,17 @@ export async function GET(request: Request): Promise<Response> {
|
|||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get("code");
|
||||||
const state = url.searchParams.get("state");
|
const state = url.searchParams.get("state");
|
||||||
const storedState = cookies().get("github_oauth_state")?.value ?? null;
|
const storedState = cookies().get("github_oauth_state")?.value ?? null;
|
||||||
|
|
||||||
|
// Log für Debugging
|
||||||
|
console.log("OAuth Callback erhalten:", url.toString());
|
||||||
|
console.log("Callback URL Validierung:", isValidCallbackHost(url.toString()));
|
||||||
|
console.log("Erlaubte Hosts:", ALLOWED_CALLBACK_HOSTS);
|
||||||
|
|
||||||
if (!code || !state || !storedState || state !== storedState) {
|
if (!code || !state || !storedState || state !== storedState) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
status_text: "Something is wrong",
|
status_text: "Ungültiger OAuth-Callback",
|
||||||
data: { code, state, storedState },
|
data: { code, state, storedState, url: url.toString() },
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
@ -34,7 +41,12 @@ export async function GET(request: Request): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// GitHub OAuth Code validieren - die redirectURI ist bereits im GitHub Client konfiguriert
|
||||||
const tokens = await github.validateAuthorizationCode(code);
|
const tokens = await github.validateAuthorizationCode(code);
|
||||||
|
|
||||||
|
// Log zur Fehlersuche
|
||||||
|
console.log(`GitHub OAuth Token-Validierung erfolgreich, verwendete Callback-URL: ${USED_CALLBACK_URL}`);
|
||||||
|
|
||||||
const githubUserResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user", {
|
const githubUserResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${tokens.accessToken}`,
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { github } from "@/server/auth/oauth";
|
import { github, USED_CALLBACK_URL } from "@/server/auth/oauth";
|
||||||
import { generateState } from "arctic";
|
import { generateState } from "arctic";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
@ -6,6 +6,9 @@ export const dynamic = "force-dynamic";
|
|||||||
|
|
||||||
export async function GET(): Promise<Response> {
|
export async function GET(): Promise<Response> {
|
||||||
const state = generateState();
|
const state = generateState();
|
||||||
|
|
||||||
|
// Verwende die zentral definierte Callback-URL
|
||||||
|
// Die redirectURI ist bereits im GitHub-Client konfiguriert
|
||||||
const url = await github.createAuthorizationURL(state, {
|
const url = await github.createAuthorizationURL(state, {
|
||||||
scopes: ["user"],
|
scopes: ["user"],
|
||||||
});
|
});
|
||||||
@ -19,5 +22,9 @@ export async function GET(): Promise<Response> {
|
|||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log zur Fehlersuche
|
||||||
|
console.log(`GitHub OAuth redirect zu: ${url.toString()}`);
|
||||||
|
console.log(`Verwendete Callback-URL: ${USED_CALLBACK_URL}`);
|
||||||
|
|
||||||
return Response.redirect(url);
|
return Response.redirect(url);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,50 @@
|
|||||||
import { GitHub } from "arctic";
|
import { GitHub } from "arctic";
|
||||||
|
import { ALLOWED_CALLBACK_HOSTS, FRONTEND_URL, OAUTH_CALLBACK_URL } from "@/utils/api-config";
|
||||||
|
|
||||||
export const github = new GitHub(process.env.OAUTH_CLIENT_ID as string, process.env.OAUTH_CLIENT_SECRET as string, {
|
// Helper-Funktion, um die passende Callback-URL zu bestimmen
|
||||||
enterpriseDomain: "https://git.i.mercedes-benz.com",
|
const getCallbackUrl = () => {
|
||||||
});
|
// Wenn eine spezifische OAuth-Callback-URL definiert ist, verwende diese
|
||||||
|
if (process.env.NEXT_PUBLIC_OAUTH_CALLBACK_URL) {
|
||||||
|
console.log("Verwende konfigurierte OAuth Callback URL:", process.env.NEXT_PUBLIC_OAUTH_CALLBACK_URL);
|
||||||
|
return process.env.NEXT_PUBLIC_OAUTH_CALLBACK_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für spezifischen Unternehmens-Hostname
|
||||||
|
if (FRONTEND_URL.includes('corpintra.net') || FRONTEND_URL.includes('m040tbaraspi001')) {
|
||||||
|
const url = `http://m040tbaraspi001.de040.corpintra.net/auth/login/callback`;
|
||||||
|
console.log("Verwende Unternehmens-Hostname für OAuth Callback:", url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback für lokale Entwicklung
|
||||||
|
console.log("Verwende Standard OAuth Callback URL:", OAUTH_CALLBACK_URL);
|
||||||
|
return OAUTH_CALLBACK_URL;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Berechne die Callback-URL
|
||||||
|
export const USED_CALLBACK_URL = getCallbackUrl();
|
||||||
|
|
||||||
|
// Erstelle GitHub OAuth-Client mit expliziter Redirect-URI
|
||||||
|
export const github = new GitHub(
|
||||||
|
process.env.OAUTH_CLIENT_ID as string,
|
||||||
|
process.env.OAUTH_CLIENT_SECRET as string,
|
||||||
|
{
|
||||||
|
enterpriseDomain: "https://git.i.mercedes-benz.com",
|
||||||
|
redirectURI: USED_CALLBACK_URL,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hilfsfunktion zur Validierung von OAuth-Callbacks
|
||||||
|
export function isValidCallbackHost(url: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
return ALLOWED_CALLBACK_HOSTS.some(host => parsedUrl.hostname === host ||
|
||||||
|
parsedUrl.hostname.includes(host));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Ungültige URL beim Validieren des Callback-Hosts:", url, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitHubUserResult {
|
export interface GitHubUserResult {
|
||||||
id: number;
|
id: number;
|
||||||
|
51
packages/reservation-platform/src/utils/api-config.ts
Normal file
51
packages/reservation-platform/src/utils/api-config.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Basis-URL für Backend-API
|
||||||
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://192.168.0.105:5000";
|
||||||
|
|
||||||
|
// Frontend-URL für Callbacks - unterstützt mehrere Domains
|
||||||
|
const getFrontendUrl = () => {
|
||||||
|
// Priorität 1: Explizit gesetzte Umgebungsvariable
|
||||||
|
if (process.env.NEXT_PUBLIC_FRONTEND_URL) {
|
||||||
|
return process.env.NEXT_PUBLIC_FRONTEND_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorität 2: Spezifischer Hostname für das Netzwerk
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// Im Browser: Prüfen auf m040tbaraspi001.de040.corpintra.net
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
if (hostname === 'm040tbaraspi001' ||
|
||||||
|
hostname === 'm040tbaraspi001.de040.corpintra.net' ||
|
||||||
|
hostname.includes('corpintra.net')) {
|
||||||
|
return `http://${hostname}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorität 3: Default für Localhost
|
||||||
|
return "http://localhost:3000";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FRONTEND_URL = getFrontendUrl();
|
||||||
|
|
||||||
|
// OAuth Callback URL - muss exakt mit der registrierten URL in GitHub übereinstimmen
|
||||||
|
export const OAUTH_CALLBACK_URL = process.env.NEXT_PUBLIC_OAUTH_CALLBACK_URL ||
|
||||||
|
`${FRONTEND_URL}/auth/login/callback`;
|
||||||
|
|
||||||
|
// Liste der erlaubten Hostnamen für OAuth-Callbacks
|
||||||
|
export const ALLOWED_CALLBACK_HOSTS = [
|
||||||
|
'localhost',
|
||||||
|
'm040tbaraspi001',
|
||||||
|
'm040tbaraspi001.de040.corpintra.net',
|
||||||
|
'192.168.0.105'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Endpunkte für die verschiedenen Ressourcen
|
||||||
|
export const API_ENDPOINTS = {
|
||||||
|
PRINTERS: `${API_BASE_URL}/api/printers`,
|
||||||
|
JOBS: `${API_BASE_URL}/api/jobs`,
|
||||||
|
USERS: `${API_BASE_URL}/api/users`,
|
||||||
|
|
||||||
|
// OAuth-spezifische Endpunkte
|
||||||
|
AUTH: {
|
||||||
|
LOGIN: `${API_BASE_URL}/api/auth/login`,
|
||||||
|
CALLBACK: `${API_BASE_URL}/api/auth/callback`,
|
||||||
|
}
|
||||||
|
};
|
78
packages/reservation-platform/src/utils/external-api.ts
Normal file
78
packages/reservation-platform/src/utils/external-api.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { API_ENDPOINTS } from './api-config';
|
||||||
|
|
||||||
|
// Typdefinitionen für API-Responses
|
||||||
|
export interface Printer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ip: string;
|
||||||
|
status: string;
|
||||||
|
is_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Job {
|
||||||
|
id: string;
|
||||||
|
printer_id: string;
|
||||||
|
user_id: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetcher für SWR mit Fehlerbehandlung
|
||||||
|
const fetchWithErrorHandling = async (url: string) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error('Ein Fehler ist bei der API-Anfrage aufgetreten');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
// API-Funktionen
|
||||||
|
export const api = {
|
||||||
|
// Drucker-Endpunkte
|
||||||
|
printers: {
|
||||||
|
getAll: () => fetchWithErrorHandling(API_ENDPOINTS.PRINTERS),
|
||||||
|
getById: (id: string) => fetchWithErrorHandling(`${API_ENDPOINTS.PRINTERS}/${id}`),
|
||||||
|
create: (data: Partial<Printer>) =>
|
||||||
|
fetch(API_ENDPOINTS.PRINTERS, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(res => res.json()),
|
||||||
|
update: (id: string, data: Partial<Printer>) =>
|
||||||
|
fetch(`${API_ENDPOINTS.PRINTERS}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(res => res.json()),
|
||||||
|
delete: (id: string) =>
|
||||||
|
fetch(`${API_ENDPOINTS.PRINTERS}/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
}).then(res => res.json()),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Jobs-Endpunkte
|
||||||
|
jobs: {
|
||||||
|
getAll: () => fetchWithErrorHandling(API_ENDPOINTS.JOBS),
|
||||||
|
getById: (id: string) => fetchWithErrorHandling(`${API_ENDPOINTS.JOBS}/${id}`),
|
||||||
|
create: (data: Partial<Job>) =>
|
||||||
|
fetch(API_ENDPOINTS.JOBS, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(res => res.json()),
|
||||||
|
update: (id: string, data: Partial<Job>) =>
|
||||||
|
fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(res => res.json()),
|
||||||
|
delete: (id: string) =>
|
||||||
|
fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
}).then(res => res.json()),
|
||||||
|
},
|
||||||
|
};
|
179
packages/reservation-platform/update-package.js
Executable file
179
packages/reservation-platform/update-package.js
Executable file
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsskript zur Aktualisierung der OAuth-Konfiguration im MYP-Frontend
|
||||||
|
*
|
||||||
|
* Dieses Skript wird automatisch beim Build ausgeführt, um sicherzustellen,
|
||||||
|
* dass die OAuth-Konfiguration korrekt ist.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Pfad zur OAuth-Konfiguration und Routes
|
||||||
|
const callbackRoutePath = path.join(__dirname, 'src/app/auth/login/callback/route.ts');
|
||||||
|
const loginRoutePath = path.join(__dirname, 'src/app/auth/login/route.ts');
|
||||||
|
const oauthConfigPath = path.join(__dirname, 'src/server/auth/oauth.ts');
|
||||||
|
|
||||||
|
// Aktualisiere die OAuth-Konfiguration
|
||||||
|
try {
|
||||||
|
// 1. Prüfe, ob wir die USED_CALLBACK_URL exportieren müssen
|
||||||
|
let oauthContent = fs.readFileSync(oauthConfigPath, 'utf8');
|
||||||
|
|
||||||
|
if (!oauthContent.includes('export const USED_CALLBACK_URL')) {
|
||||||
|
console.log('✅ Aktualisiere OAuth-Konfiguration...');
|
||||||
|
|
||||||
|
// Füge die USED_CALLBACK_URL-Export hinzu
|
||||||
|
oauthContent = oauthContent.replace(
|
||||||
|
'// Erstelle GitHub OAuth-Client mit expliziter Redirect-URI',
|
||||||
|
'// Berechne die Callback-URL\nexport const USED_CALLBACK_URL = getCallbackUrl();\n\n// Erstelle GitHub OAuth-Client mit expliziter Redirect-URI'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schreibe die aktualisierte Datei
|
||||||
|
fs.writeFileSync(oauthConfigPath, oauthContent, 'utf8');
|
||||||
|
console.log('✅ OAuth-Konfiguration erfolgreich aktualisiert.');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ OAuth-Konfiguration ist bereits aktuell.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Aktualisieren der OAuth-Konfiguration:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere die OAuth-Callback-Route
|
||||||
|
try {
|
||||||
|
let callbackContent = fs.readFileSync(callbackRoutePath, 'utf8');
|
||||||
|
|
||||||
|
// Prüfe, ob Änderungen nötig sind
|
||||||
|
const needsUpdate =
|
||||||
|
callbackContent.includes('await github.validateAuthorizationCode(code, OAUTH_CALLBACK_URL)') ||
|
||||||
|
!callbackContent.includes('USED_CALLBACK_URL');
|
||||||
|
|
||||||
|
if (needsUpdate) {
|
||||||
|
console.log('✅ Aktualisiere OAuth-Callback-Route...');
|
||||||
|
|
||||||
|
// 1. Aktualisiere den Import
|
||||||
|
if (!callbackContent.includes('USED_CALLBACK_URL')) {
|
||||||
|
callbackContent = callbackContent.replace(
|
||||||
|
'import { type GitHubUserResult, github, isValidCallbackHost } from "@/server/auth/oauth";',
|
||||||
|
'import { type GitHubUserResult, github, isValidCallbackHost, USED_CALLBACK_URL } from "@/server/auth/oauth";'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Entferne den OAUTH_CALLBACK_URL-Import, wenn er nicht mehr benötigt wird
|
||||||
|
if (callbackContent.includes('OAUTH_CALLBACK_URL')) {
|
||||||
|
callbackContent = callbackContent.replace(
|
||||||
|
', OAUTH_CALLBACK_URL } from "@/utils/api-config"',
|
||||||
|
' } from "@/utils/api-config"'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Korrigiere die validateAuthorizationCode-Funktion
|
||||||
|
if (callbackContent.includes('await github.validateAuthorizationCode(code, OAUTH_CALLBACK_URL)')) {
|
||||||
|
callbackContent = callbackContent.replace(
|
||||||
|
'await github.validateAuthorizationCode(code, OAUTH_CALLBACK_URL)',
|
||||||
|
'await github.validateAuthorizationCode(code)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Aktualisiere die Logging-Nachricht
|
||||||
|
if (callbackContent.includes('console.log(`GitHub OAuth Token-Validierung mit Callback-URL: ${OAUTH_CALLBACK_URL}`)')) {
|
||||||
|
callbackContent = callbackContent.replace(
|
||||||
|
'console.log(`GitHub OAuth Token-Validierung mit Callback-URL: ${OAUTH_CALLBACK_URL}`)',
|
||||||
|
'console.log(`GitHub OAuth Token-Validierung erfolgreich, verwendete Callback-URL: ${USED_CALLBACK_URL}`)'
|
||||||
|
);
|
||||||
|
} else if (callbackContent.includes('console.log("GitHub OAuth Token-Validierung erfolgreich")')) {
|
||||||
|
callbackContent = callbackContent.replace(
|
||||||
|
'console.log("GitHub OAuth Token-Validierung erfolgreich")',
|
||||||
|
'console.log(`GitHub OAuth Token-Validierung erfolgreich, verwendete Callback-URL: ${USED_CALLBACK_URL}`)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schreibe die aktualisierte Datei
|
||||||
|
fs.writeFileSync(callbackRoutePath, callbackContent, 'utf8');
|
||||||
|
console.log('✅ OAuth-Callback-Route erfolgreich aktualisiert.');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ OAuth-Callback-Route ist bereits aktuell.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Aktualisieren der OAuth-Callback-Route:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package.json aktualisieren, um das Skript vor dem Build auszuführen
|
||||||
|
try {
|
||||||
|
const packageJsonPath = path.join(__dirname, 'package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
|
// Prüfe, ob das Skript bereits in den Build-Prozess integriert ist
|
||||||
|
if (packageJson.scripts.build === 'next build') {
|
||||||
|
console.log('✅ Aktualisiere package.json...');
|
||||||
|
|
||||||
|
// Füge das Skript zum Build-Prozess hinzu
|
||||||
|
packageJson.scripts.build = 'node update-package.js && next build';
|
||||||
|
|
||||||
|
// Schreibe die aktualisierte package.json
|
||||||
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
|
||||||
|
console.log('✅ package.json erfolgreich aktualisiert.');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ package.json ist bereits aktualisiert.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Aktualisieren der package.json:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere die Login-Route
|
||||||
|
try {
|
||||||
|
let loginContent = fs.readFileSync(loginRoutePath, 'utf8');
|
||||||
|
|
||||||
|
// Prüfe, ob Änderungen nötig sind
|
||||||
|
const loginNeedsUpdate =
|
||||||
|
loginContent.includes('redirectURI: OAUTH_CALLBACK_URL') ||
|
||||||
|
!loginContent.includes('USED_CALLBACK_URL');
|
||||||
|
|
||||||
|
if (loginNeedsUpdate) {
|
||||||
|
console.log('✅ Aktualisiere OAuth-Login-Route...');
|
||||||
|
|
||||||
|
// 1. Aktualisiere den Import
|
||||||
|
if (!loginContent.includes('USED_CALLBACK_URL')) {
|
||||||
|
if (loginContent.includes('import { github } from "@/server/auth/oauth";')) {
|
||||||
|
loginContent = loginContent.replace(
|
||||||
|
'import { github } from "@/server/auth/oauth";',
|
||||||
|
'import { github, USED_CALLBACK_URL } from "@/server/auth/oauth";'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entferne den OAUTH_CALLBACK_URL-Import
|
||||||
|
if (loginContent.includes('import { OAUTH_CALLBACK_URL } from "@/utils/api-config";')) {
|
||||||
|
loginContent = loginContent.replace(
|
||||||
|
'import { OAUTH_CALLBACK_URL } from "@/utils/api-config";',
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Korrigiere die createAuthorizationURL-Funktion
|
||||||
|
if (loginContent.includes('redirectURI: OAUTH_CALLBACK_URL')) {
|
||||||
|
loginContent = loginContent.replace(
|
||||||
|
/const url = await github\.createAuthorizationURL\(state, \{\s*scopes: \["user"\],\s*redirectURI: OAUTH_CALLBACK_URL,\s*\}\);/s,
|
||||||
|
'const url = await github.createAuthorizationURL(state, {\n\t\tscopes: ["user"],\n\t});'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Aktualisiere die Logging-Nachricht
|
||||||
|
if (loginContent.includes('console.log(`Verwendete Callback-URL: ${OAUTH_CALLBACK_URL}`')) {
|
||||||
|
loginContent = loginContent.replace(
|
||||||
|
'console.log(`Verwendete Callback-URL: ${OAUTH_CALLBACK_URL}`',
|
||||||
|
'console.log(`Verwendete Callback-URL: ${USED_CALLBACK_URL}`'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schreibe die aktualisierte Datei
|
||||||
|
fs.writeFileSync(loginRoutePath, loginContent, 'utf8');
|
||||||
|
console.log('✅ OAuth-Login-Route erfolgreich aktualisiert.');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ OAuth-Login-Route ist bereits aktuell.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Aktualisieren der OAuth-Login-Route:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ OAuth-Konfiguration wurde erfolgreich vorbereitet.');
|
225
raspi-cleanup.sh
Executable file
225
raspi-cleanup.sh
Executable file
@ -0,0 +1,225 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Raspberry Pi Bereinigungsskript für MYP-Projekt
|
||||||
|
# Dieses Skript bereinigt alte Docker-Installationen und installiert alle erforderlichen Abhängigkeiten
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktion zur Ausgabe mit Zeitstempel
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfen, ob das Skript mit Root-Rechten ausgeführt wird
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
error_log "Dieses Skript muss mit Root-Rechten ausgeführt werden (sudo)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${YELLOW}=== MYP Raspberry Pi Bereinigung und Setup ===${NC}"
|
||||||
|
log "Diese Skript wird alle alten Docker-Installationen entfernen und die erforderlichen Abhängigkeiten neu installieren."
|
||||||
|
|
||||||
|
# Sicherstellen, dass apt funktioniert
|
||||||
|
log "Aktualisiere apt-Paketindex..."
|
||||||
|
apt-get update || {
|
||||||
|
error_log "Konnte apt-Paketindex nicht aktualisieren."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installiere grundlegende Abhängigkeiten
|
||||||
|
log "Installiere grundlegende Abhängigkeiten..."
|
||||||
|
apt-get install -y \
|
||||||
|
apt-transport-https \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gnupg \
|
||||||
|
lsb-release \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
jq \
|
||||||
|
|| {
|
||||||
|
error_log "Konnte grundlegende Abhängigkeiten nicht installieren."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stoppe alle laufenden Docker-Container
|
||||||
|
log "${YELLOW}Stoppe alle laufenden Docker-Container...${NC}"
|
||||||
|
if command -v docker &> /dev/null; then
|
||||||
|
docker stop $(docker ps -aq) 2>/dev/null || true
|
||||||
|
log "Alle Docker-Container gestoppt."
|
||||||
|
else
|
||||||
|
log "Docker ist nicht installiert, keine Container zu stoppen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Entferne alte Docker-Installation
|
||||||
|
log "${YELLOW}Entferne alte Docker-Installation...${NC}"
|
||||||
|
apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-compose || true
|
||||||
|
apt-get autoremove -y || true
|
||||||
|
rm -rf /var/lib/docker /var/lib/containerd /var/run/docker.sock /etc/docker /usr/local/bin/docker-compose 2>/dev/null || true
|
||||||
|
log "${GREEN}Alte Docker-Installation entfernt.${NC}"
|
||||||
|
|
||||||
|
# Entferne alte Projektcontainer und -Dateien
|
||||||
|
log "${YELLOW}Entferne alte MYP-Projektcontainer und -Dateien...${NC}"
|
||||||
|
if command -v docker &> /dev/null; then
|
||||||
|
# Entferne Container
|
||||||
|
docker rm -f myp-frontend myp-backend 2>/dev/null || true
|
||||||
|
# Entferne Images
|
||||||
|
docker rmi -f myp-frontend myp-backend 2>/dev/null || true
|
||||||
|
# Entferne unbenutzte Volumes und Netzwerke
|
||||||
|
docker system prune -af --volumes 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erkennen der Raspberry Pi-Architektur
|
||||||
|
log "Erkenne Systemarchitektur..."
|
||||||
|
ARCH=$(dpkg --print-architecture)
|
||||||
|
log "Erkannte Architektur: ${ARCH}"
|
||||||
|
|
||||||
|
# Installiere Docker mit dem offiziellen Convenience-Skript
|
||||||
|
log "${YELLOW}Installiere Docker mit dem offiziellen Convenience-Skript...${NC}"
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sh get-docker.sh --channel stable
|
||||||
|
|
||||||
|
# Überprüfen, ob Docker erfolgreich installiert wurde
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
error_log "Docker-Installation fehlgeschlagen!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}Docker erfolgreich installiert!${NC}"
|
||||||
|
|
||||||
|
# Füge den aktuellen Benutzer zur Docker-Gruppe hinzu
|
||||||
|
if [ "$SUDO_USER" ]; then
|
||||||
|
log "Füge Benutzer $SUDO_USER zur Docker-Gruppe hinzu..."
|
||||||
|
usermod -aG docker $SUDO_USER
|
||||||
|
log "${YELLOW}Hinweis: Eine Neuanmeldung ist erforderlich, damit die Gruppenänderung wirksam wird.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfiguriere Docker mit DNS-Servern für bessere Netzwerkkompatibilität
|
||||||
|
log "Konfiguriere Docker mit Google DNS..."
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
cat > /etc/docker/daemon.json << EOL
|
||||||
|
{
|
||||||
|
"dns": ["8.8.8.8", "8.8.4.4"]
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Starte Docker-Dienst neu
|
||||||
|
log "Starte Docker-Dienst neu..."
|
||||||
|
systemctl restart docker
|
||||||
|
systemctl enable docker
|
||||||
|
log "${GREEN}Docker-Dienst neu gestartet und für den Autostart aktiviert.${NC}"
|
||||||
|
|
||||||
|
# Installiere Docker Compose v2
|
||||||
|
log "${YELLOW}Installiere Docker Compose...${NC}"
|
||||||
|
|
||||||
|
# Bestimme die passende Docker Compose-Version für die Architektur
|
||||||
|
if [ "$ARCH" = "armhf" ]; then
|
||||||
|
log "Installiere Docker Compose für armhf (32-bit)..."
|
||||||
|
curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose
|
||||||
|
elif [ "$ARCH" = "arm64" ]; then
|
||||||
|
log "Installiere Docker Compose für arm64 (64-bit)..."
|
||||||
|
curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
|
||||||
|
else
|
||||||
|
log "Unbekannte Architektur, verwende automatische Erkennung..."
|
||||||
|
curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
# Überprüfe, ob Docker Compose installiert wurde
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
error_log "Docker Compose-Installation fehlgeschlagen!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installiere Docker Compose Plugin (neuere Methode)
|
||||||
|
log "Installiere Docker Compose Plugin..."
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y docker-compose-plugin
|
||||||
|
|
||||||
|
log "${GREEN}Docker Compose erfolgreich installiert!${NC}"
|
||||||
|
docker compose version || docker-compose --version
|
||||||
|
|
||||||
|
# Installiere zusätzliche Abhängigkeiten für die Projektunterstützung
|
||||||
|
log "${YELLOW}Installiere zusätzliche Projektabhängigkeiten...${NC}"
|
||||||
|
apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
sqlite3 \
|
||||||
|
build-essential \
|
||||||
|
libffi-dev \
|
||||||
|
libssl-dev \
|
||||||
|
|| {
|
||||||
|
error_log "Konnte zusätzliche Abhängigkeiten nicht installieren."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimieren des Raspberry Pi für Docker-Workloads
|
||||||
|
log "${YELLOW}Optimiere Raspberry Pi für Docker-Workloads...${NC}"
|
||||||
|
|
||||||
|
# Swap erhöhen für bessere Performance bei begrenztem RAM
|
||||||
|
log "Konfiguriere Swap-Größe..."
|
||||||
|
CURRENT_SWAP=$(grep "CONF_SWAPSIZE" /etc/dphys-swapfile | cut -d= -f2)
|
||||||
|
log "Aktuelle Swap-Größe: ${CURRENT_SWAP}"
|
||||||
|
|
||||||
|
# Erhöhe Swap auf 2GB, wenn weniger
|
||||||
|
if [ "$CURRENT_SWAP" -lt 2048 ]; then
|
||||||
|
sed -i 's/^CONF_SWAPSIZE=.*/CONF_SWAPSIZE=2048/' /etc/dphys-swapfile
|
||||||
|
log "Swap-Größe auf 2048MB erhöht, Neustart des Swap-Dienstes erforderlich."
|
||||||
|
|
||||||
|
# Neustart des Swap-Dienstes
|
||||||
|
/etc/init.d/dphys-swapfile restart
|
||||||
|
else
|
||||||
|
log "Swap-Größe ist bereits ausreichend."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfiguriere cgroup für Docker
|
||||||
|
if ! grep -q "cgroup_enable=memory" /boot/cmdline.txt; then
|
||||||
|
log "Konfiguriere cgroup für Docker..."
|
||||||
|
CMDLINE=$(cat /boot/cmdline.txt)
|
||||||
|
echo "$CMDLINE cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1" > /boot/cmdline.txt
|
||||||
|
log "${YELLOW}WICHTIG: Ein Systemneustart ist erforderlich, damit die cgroup-Änderungen wirksam werden.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob docker-compose.yml und Dockerfile im aktuellen Projektverzeichnis vorhanden sind
|
||||||
|
FRONTEND_DIR="$(pwd)/packages/reservation-platform"
|
||||||
|
BACKEND_DIR="$(pwd)/backend"
|
||||||
|
|
||||||
|
if [ -d "$FRONTEND_DIR" ] && [ -f "$FRONTEND_DIR/docker-compose.yml" ]; then
|
||||||
|
log "${GREEN}Frontend-Projektdateien gefunden in $FRONTEND_DIR${NC}"
|
||||||
|
else
|
||||||
|
log "${YELLOW}Warnung: Frontend-Projektdateien nicht gefunden in $FRONTEND_DIR${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$BACKEND_DIR" ] && [ -f "$BACKEND_DIR/docker-compose.yml" ]; then
|
||||||
|
log "${GREEN}Backend-Projektdateien gefunden in $BACKEND_DIR${NC}"
|
||||||
|
else
|
||||||
|
log "${YELLOW}Warnung: Backend-Projektdateien nicht gefunden in $BACKEND_DIR${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Abschlussmeldung
|
||||||
|
log "${GREEN}=== Bereinigung und Setup abgeschlossen ===${NC}"
|
||||||
|
log "${YELLOW}WICHTIGE HINWEISE:${NC}"
|
||||||
|
log "1. Ein ${RED}SYSTEMNEUSTART${NC} ist ${RED}DRINGEND ERFORDERLICH${NC}, damit alle Änderungen wirksam werden."
|
||||||
|
log "2. Nach dem Neustart können Sie die Installationsskripte ausführen:"
|
||||||
|
log " - Frontend: ./install-frontend.sh"
|
||||||
|
log " - Backend: ./install-backend.sh"
|
||||||
|
log "3. Bei Problemen mit Docker-Berechtigungen stellen Sie sicher, dass Sie sich neu angemeldet haben."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
read -p "Möchten Sie das System jetzt neu starten? (j/n): " REBOOT_CHOICE
|
||||||
|
if [[ "$REBOOT_CHOICE" == "j" ]]; then
|
||||||
|
log "System wird neu gestartet..."
|
||||||
|
reboot
|
||||||
|
else
|
||||||
|
log "Bitte starten Sie das System manuell neu, bevor Sie die Installationsskripte ausführen."
|
||||||
|
fi
|
567
raspi-frontend-deploy.sh
Executable file
567
raspi-frontend-deploy.sh
Executable file
@ -0,0 +1,567 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Frontend-Deployment-Skript für MYP-Projekt
|
||||||
|
# Bietet verschiedene Möglichkeiten, das Frontend zu deployen
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktion zur Ausgabe mit Zeitstempel
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
success_log() {
|
||||||
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ERFOLG:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}===== $1 =====${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Variablen definieren
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
FRONTEND_DIR="$SCRIPT_DIR/packages/reservation-platform"
|
||||||
|
DOCKER_DIR="$FRONTEND_DIR/docker"
|
||||||
|
DEFAULT_BACKEND_URL="http://192.168.0.105:5000"
|
||||||
|
IMAGE_NAME="myp-rp:latest"
|
||||||
|
CONTAINER_NAME="myp-rp"
|
||||||
|
DB_VOLUME_DIR="/srv/MYP-DB"
|
||||||
|
ENV_FILE_PATH="/srv/myp-env/github.env"
|
||||||
|
|
||||||
|
# Prüfen, ob wir im Root des Projektverzeichnisses sind
|
||||||
|
if [ ! -d "packages/reservation-platform" ]; then
|
||||||
|
error_log "Dieses Skript muss im Root-Verzeichnis des MYP-Projekts ausgeführt werden."
|
||||||
|
error_log "Bitte wechseln Sie in das Verzeichnis, das die 'packages/reservation-platform' enthält."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob Docker installiert ist
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
error_log "Docker ist nicht installiert. Bitte installieren Sie Docker zuerst."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob Docker läuft
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
error_log "Docker-Daemon läuft nicht. Bitte starten Sie Docker mit 'sudo systemctl start docker'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob der Benutzer in der Docker-Gruppe ist
|
||||||
|
if ! groups | grep -q '\bdocker\b'; then
|
||||||
|
error_log "Aktueller Benutzer hat keine Docker-Berechtigungen."
|
||||||
|
error_log "Bitte führen Sie das Skript mit 'sudo' aus oder fügen Sie den Benutzer zur Docker-Gruppe hinzu:"
|
||||||
|
error_log "sudo usermod -aG docker $USER && newgrp docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle Datenbank-Verzeichnis, falls nicht vorhanden
|
||||||
|
if [ ! -d "$DB_VOLUME_DIR" ]; then
|
||||||
|
log "Erstelle Datenbankverzeichnis: $DB_VOLUME_DIR"
|
||||||
|
if ! mkdir -p "$DB_VOLUME_DIR"; then
|
||||||
|
if ! sudo mkdir -p "$DB_VOLUME_DIR"; then
|
||||||
|
error_log "Konnte Datenbankverzeichnis nicht erstellen. Bitte erstellen Sie es manuell:"
|
||||||
|
error_log "sudo mkdir -p $DB_VOLUME_DIR && sudo chown $USER:$USER $DB_VOLUME_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sudo chown $USER:$USER "$DB_VOLUME_DIR"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Datenbankverzeichnis existiert bereits: $DB_VOLUME_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Funktion zum Laden der Umgebungsvariablen aus /srv/myp-env/github.env
|
||||||
|
load_env_from_srv() {
|
||||||
|
if [ -f "$ENV_FILE_PATH" ]; then
|
||||||
|
log "Lade Umgebungsvariablen aus $ENV_FILE_PATH"
|
||||||
|
|
||||||
|
# Versuche, die Variablen aus der Datei zu laden
|
||||||
|
OAUTH_CLIENT_ID=$(grep -oP 'OAUTH_CLIENT_ID=\K.*' "$ENV_FILE_PATH" 2>/dev/null || echo "client_id")
|
||||||
|
OAUTH_CLIENT_SECRET=$(grep -oP 'OAUTH_CLIENT_SECRET=\K.*' "$ENV_FILE_PATH" 2>/dev/null || echo "client_secret")
|
||||||
|
|
||||||
|
# Prüfe, ob die Backend-URL in der Datei definiert ist
|
||||||
|
BACKEND_URL_FROM_FILE=$(grep -oP 'NEXT_PUBLIC_API_URL=\K.*' "$ENV_FILE_PATH" 2>/dev/null)
|
||||||
|
if [ -n "$BACKEND_URL_FROM_FILE" ]; then
|
||||||
|
log "Backend-URL aus $ENV_FILE_PATH geladen: $BACKEND_URL_FROM_FILE"
|
||||||
|
DEFAULT_BACKEND_URL="$BACKEND_URL_FROM_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "OAuth-Konfiguration aus $ENV_FILE_PATH geladen."
|
||||||
|
else
|
||||||
|
log "${YELLOW}Warnung: $ENV_FILE_PATH nicht gefunden. Verwende Standard-Konfiguration.${NC}"
|
||||||
|
OAUTH_CLIENT_ID="client_id"
|
||||||
|
OAUTH_CLIENT_SECRET="client_secret"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Konfigurieren der Backend-URL
|
||||||
|
configure_backend_url() {
|
||||||
|
local backend_url="${1:-$DEFAULT_BACKEND_URL}"
|
||||||
|
|
||||||
|
header "Backend-URL konfigurieren"
|
||||||
|
log "Konfiguriere Backend-URL für Frontend: $backend_url"
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv
|
||||||
|
load_env_from_srv
|
||||||
|
|
||||||
|
# Prüfen, ob setup-backend-url.sh existiert
|
||||||
|
if [ -f "$FRONTEND_DIR/setup-backend-url.sh" ]; then
|
||||||
|
chmod +x "$FRONTEND_DIR/setup-backend-url.sh"
|
||||||
|
if ! "$FRONTEND_DIR/setup-backend-url.sh" "$backend_url"; then
|
||||||
|
error_log "Fehler beim Konfigurieren der Backend-URL."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Bestimme den Hostnamen für OAuth
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
||||||
|
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||||
|
OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
|
||||||
|
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
else
|
||||||
|
FRONTEND_HOSTNAME="$HOSTNAME"
|
||||||
|
OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback"
|
||||||
|
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle .env.local-Datei manuell
|
||||||
|
log "Erstelle .env.local-Datei manuell..."
|
||||||
|
cat > "$FRONTEND_DIR/.env.local" << EOL
|
||||||
|
# Backend API Konfiguration
|
||||||
|
NEXT_PUBLIC_API_URL=${backend_url}
|
||||||
|
|
||||||
|
# Frontend-URL für OAuth Callback
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
||||||
|
|
||||||
|
# Explizite OAuth Callback URL für GitHub
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
||||||
|
|
||||||
|
# OAuth Konfiguration aus /srv/myp-env/github.env
|
||||||
|
OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
||||||
|
OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
if [ ! -f "$FRONTEND_DIR/.env.local" ]; then
|
||||||
|
error_log "Konnte .env.local-Datei nicht erstellen."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 600 "$FRONTEND_DIR/.env.local"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Backend-URL erfolgreich konfiguriert: $backend_url"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Bauen des Images
|
||||||
|
build_image() {
|
||||||
|
header "Docker-Image bauen"
|
||||||
|
log "Baue Docker-Image: $IMAGE_NAME"
|
||||||
|
|
||||||
|
if [ ! -f "$FRONTEND_DIR/Dockerfile" ]; then
|
||||||
|
error_log "Dockerfile nicht gefunden in $FRONTEND_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$FRONTEND_DIR" || return 1
|
||||||
|
|
||||||
|
# Vorhandenes Image entfernen, falls gewünscht
|
||||||
|
if docker image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||||
|
log "Image $IMAGE_NAME existiert bereits."
|
||||||
|
read -p "Möchten Sie das existierende Image überschreiben? (j/n): " rebuild_choice
|
||||||
|
if [[ "$rebuild_choice" == "j" ]]; then
|
||||||
|
log "Entferne existierendes Image..."
|
||||||
|
docker rmi "$IMAGE_NAME" &>/dev/null || true
|
||||||
|
else
|
||||||
|
log "Behalte existierendes Image."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Baue das Image
|
||||||
|
log "${YELLOW}Baue Docker-Image... (Dies kann auf einem Raspberry Pi mehrere Minuten dauern)${NC}"
|
||||||
|
if ! docker build -t "$IMAGE_NAME" .; then
|
||||||
|
error_log "Fehler beim Bauen des Docker-Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Docker-Image erfolgreich gebaut: $IMAGE_NAME"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Speichern des Images
|
||||||
|
save_image() {
|
||||||
|
header "Docker-Image speichern"
|
||||||
|
local save_dir="${1:-$DOCKER_DIR/images}"
|
||||||
|
local save_file="$save_dir/myp-frontend.tar"
|
||||||
|
|
||||||
|
# Prüfen, ob das Image existiert
|
||||||
|
if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||||
|
error_log "Image $IMAGE_NAME existiert nicht. Bitte bauen Sie es zuerst."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verzeichnis erstellen, falls es nicht existiert
|
||||||
|
mkdir -p "$save_dir"
|
||||||
|
|
||||||
|
log "Speichere Docker-Image in: $save_file"
|
||||||
|
log "${YELLOW}Dies kann einige Minuten dauern...${NC}"
|
||||||
|
|
||||||
|
if ! docker save -o "$save_file" "$IMAGE_NAME"; then
|
||||||
|
error_log "Fehler beim Speichern des Docker-Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob die Datei erstellt wurde
|
||||||
|
if [ ! -f "$save_file" ]; then
|
||||||
|
error_log "Konnte Docker-Image nicht speichern."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe Dateigröße
|
||||||
|
local filesize=$(stat -c%s "$save_file")
|
||||||
|
if [ "$filesize" -lt 1000000 ]; then # Kleiner als 1MB ist verdächtig
|
||||||
|
error_log "Gespeichertes Image ist ungewöhnlich klein ($filesize Bytes). Möglicherweise ist etwas schief gelaufen."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Docker-Image erfolgreich gespeichert: $save_file (Größe: $(du -h "$save_file" | cut -f1))"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Laden des Images
|
||||||
|
load_image() {
|
||||||
|
header "Docker-Image laden"
|
||||||
|
local load_dir="${1:-$DOCKER_DIR/images}"
|
||||||
|
local load_file="$load_dir/myp-frontend.tar"
|
||||||
|
|
||||||
|
# Prüfen, ob die Datei existiert
|
||||||
|
if [ ! -f "$load_file" ]; then
|
||||||
|
error_log "Image-Datei nicht gefunden: $load_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe Dateigröße
|
||||||
|
local filesize=$(stat -c%s "$load_file")
|
||||||
|
if [ "$filesize" -lt 1000000 ]; then # Kleiner als 1MB ist verdächtig
|
||||||
|
error_log "Image-Datei ist ungewöhnlich klein ($filesize Bytes). Möglicherweise ist sie beschädigt."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Lade Docker-Image aus: $load_file"
|
||||||
|
log "${YELLOW}Dies kann einige Minuten dauern...${NC}"
|
||||||
|
|
||||||
|
if ! docker load -i "$load_file"; then
|
||||||
|
error_log "Fehler beim Laden des Docker-Images. Die Datei könnte beschädigt sein."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Docker-Image erfolgreich geladen."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Starten des Containers mit Docker Compose
|
||||||
|
start_container_compose() {
|
||||||
|
header "Container mit Docker Compose starten"
|
||||||
|
|
||||||
|
# Erstellen der vereinfachten docker-compose.yml-Datei
|
||||||
|
local compose_file="$DOCKER_DIR/compose.simple.yml"
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv, falls noch nicht geschehen
|
||||||
|
if [ -z "$OAUTH_CLIENT_ID" ]; then
|
||||||
|
load_env_from_srv
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Erstelle vereinfachte Docker-Compose-Datei: $compose_file"
|
||||||
|
cat > "$compose_file" << EOL
|
||||||
|
services:
|
||||||
|
myp-rp:
|
||||||
|
image: ${IMAGE_NAME}
|
||||||
|
container_name: ${CONTAINER_NAME}
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
||||||
|
- NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
||||||
|
- NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
||||||
|
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
||||||
|
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
||||||
|
env_file: "${ENV_FILE_PATH}"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ${DB_VOLUME_DIR}:/app/db
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Prüfen, ob die Datei erstellt wurde
|
||||||
|
if [ ! -f "$compose_file" ]; then
|
||||||
|
error_log "Konnte Docker-Compose-Datei nicht erstellen."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stoppen des vorhandenen Containers
|
||||||
|
if docker ps -a | grep -q "$CONTAINER_NAME"; then
|
||||||
|
log "Stoppe und entferne existierenden Container..."
|
||||||
|
docker stop "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
docker rm "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Container starten
|
||||||
|
log "Starte Container..."
|
||||||
|
cd "$DOCKER_DIR" || return 1
|
||||||
|
|
||||||
|
if ! docker compose -f "$(basename "$compose_file")" up -d; then
|
||||||
|
# Versuche mit docker-compose, falls docker compose nicht funktioniert
|
||||||
|
if ! docker-compose -f "$(basename "$compose_file")" up -d; then
|
||||||
|
error_log "Fehler beim Starten des Containers."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Container erfolgreich gestartet: $CONTAINER_NAME"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Starten des Containers mit Docker Run
|
||||||
|
start_container_run() {
|
||||||
|
header "Container direkt starten"
|
||||||
|
|
||||||
|
# Stoppen des vorhandenen Containers
|
||||||
|
if docker ps -a | grep -q "$CONTAINER_NAME"; then
|
||||||
|
log "Stoppe und entferne existierenden Container..."
|
||||||
|
docker stop "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
docker rm "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv, falls noch nicht geschehen
|
||||||
|
if [ -z "$OAUTH_CLIENT_ID" ]; then
|
||||||
|
load_env_from_srv
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Container starten
|
||||||
|
log "Starte Container mit 'docker run'..."
|
||||||
|
|
||||||
|
if ! docker run -d --name "$CONTAINER_NAME" \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e "NEXT_PUBLIC_API_URL=$BACKEND_URL" \
|
||||||
|
-e "NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}" \
|
||||||
|
-e "NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}" \
|
||||||
|
-e "OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}" \
|
||||||
|
-e "OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}" \
|
||||||
|
--env-file "$ENV_FILE_PATH" \
|
||||||
|
-v "$DB_VOLUME_DIR:/app/db" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
"$IMAGE_NAME"; then
|
||||||
|
error_log "Fehler beim Starten des Containers."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Container erfolgreich gestartet: $CONTAINER_NAME"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Starten der Anwendung ohne Docker
|
||||||
|
start_without_docker() {
|
||||||
|
header "Anwendung ohne Docker starten"
|
||||||
|
|
||||||
|
cd "$FRONTEND_DIR" || return 1
|
||||||
|
|
||||||
|
# Prüfen, ob Node.js und pnpm installiert sind
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
error_log "Node.js ist nicht installiert. Bitte installieren Sie Node.js zuerst."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v pnpm &> /dev/null; then
|
||||||
|
log "pnpm ist nicht installiert. Installiere pnpm..."
|
||||||
|
npm install -g pnpm
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Fehler beim Installieren von pnpm."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installiere Abhängigkeiten
|
||||||
|
log "Installiere Abhängigkeiten..."
|
||||||
|
if ! pnpm install; then
|
||||||
|
error_log "Fehler beim Installieren der Abhängigkeiten."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv und konfiguriere die Backend-URL
|
||||||
|
load_env_from_srv
|
||||||
|
if ! configure_backend_url "$BACKEND_URL"; then
|
||||||
|
error_log "Fehler beim Konfigurieren der Backend-URL."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Baue und starte die Anwendung
|
||||||
|
log "Baue und starte die Anwendung..."
|
||||||
|
if ! pnpm build; then
|
||||||
|
log "${YELLOW}Warnung: Build fehlgeschlagen. Versuche, im Dev-Modus zu starten...${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Starte im Screen-Session, damit die Anwendung im Hintergrund läuft
|
||||||
|
if command -v screen &> /dev/null; then
|
||||||
|
log "Starte Anwendung in Screen-Session..."
|
||||||
|
screen -dmS myp-frontend bash -c "cd $FRONTEND_DIR && \
|
||||||
|
NEXT_PUBLIC_API_URL=$BACKEND_URL \
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} \
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} \
|
||||||
|
OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} \
|
||||||
|
OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} \
|
||||||
|
pnpm start || \
|
||||||
|
NEXT_PUBLIC_API_URL=$BACKEND_URL \
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} \
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} \
|
||||||
|
OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} \
|
||||||
|
OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} \
|
||||||
|
pnpm dev"
|
||||||
|
success_log "Anwendung im Hintergrund gestartet. Verbinden mit: screen -r myp-frontend"
|
||||||
|
else
|
||||||
|
log "${YELLOW}Screen ist nicht installiert. Starte Anwendung im Vordergrund...${NC}"
|
||||||
|
log "${YELLOW}Beenden mit Strg+C. Die Anwendung wird dann beendet.${NC}"
|
||||||
|
sleep 3
|
||||||
|
export NEXT_PUBLIC_API_URL="$BACKEND_URL"
|
||||||
|
export NEXT_PUBLIC_FRONTEND_URL="http://${FRONTEND_HOSTNAME}"
|
||||||
|
export NEXT_PUBLIC_OAUTH_CALLBACK_URL="${OAUTH_URL}"
|
||||||
|
export OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID}"
|
||||||
|
export OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET}"
|
||||||
|
pnpm start || pnpm dev
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion für das Hauptmenü
|
||||||
|
main_menu() {
|
||||||
|
local choice
|
||||||
|
header "MYP Frontend Deployment"
|
||||||
|
echo "Bitte wählen Sie eine Option:"
|
||||||
|
echo ""
|
||||||
|
echo "1) Alles automatisch (Build, Deploy, Starten)"
|
||||||
|
echo "2) Docker-Image bauen"
|
||||||
|
echo "3) Docker-Image speichern"
|
||||||
|
echo "4) Docker-Image laden"
|
||||||
|
echo "5) Container mit Docker Compose starten"
|
||||||
|
echo "6) Container direkt mit Docker Run starten"
|
||||||
|
echo "7) Anwendung ohne Docker starten"
|
||||||
|
echo "8) Nur Backend-URL konfigurieren"
|
||||||
|
echo "9) Beenden"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Wahl (1-9): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) auto_deploy ;;
|
||||||
|
2) build_image ;;
|
||||||
|
3) save_image ;;
|
||||||
|
4) load_image ;;
|
||||||
|
5) configure_backend_url && start_container_compose ;;
|
||||||
|
6) configure_backend_url && start_container_run ;;
|
||||||
|
7) start_without_docker ;;
|
||||||
|
8) configure_backend_url ;;
|
||||||
|
9) log "Beende das Programm." && exit 0 ;;
|
||||||
|
*) error_log "Ungültige Auswahl. Bitte versuchen Sie es erneut." && main_menu ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Zurück zum Hauptmenü, es sei denn, der Benutzer hat das Programm beendet
|
||||||
|
if [ $choice -ne 9 ]; then
|
||||||
|
read -p "Drücken Sie Enter, um zum Hauptmenü zurückzukehren..."
|
||||||
|
main_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Automatischer Deployment-Workflow
|
||||||
|
auto_deploy() {
|
||||||
|
header "Automatisches Deployment"
|
||||||
|
log "Starte automatischen Deployment-Workflow..."
|
||||||
|
|
||||||
|
# Konfiguriere Backend-URL
|
||||||
|
if ! configure_backend_url; then
|
||||||
|
error_log "Fehler beim Konfigurieren der Backend-URL."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Versuche zunächst, das Image zu laden
|
||||||
|
local load_dir="$DOCKER_DIR/images"
|
||||||
|
local load_file="$load_dir/myp-frontend.tar"
|
||||||
|
|
||||||
|
if [ -f "$load_file" ]; then
|
||||||
|
log "Image-Datei gefunden. Versuche zu laden..."
|
||||||
|
if load_image; then
|
||||||
|
log "Image erfolgreich geladen. Überspringe Bauen."
|
||||||
|
else
|
||||||
|
log "Konnte Image nicht laden. Versuche zu bauen..."
|
||||||
|
if ! build_image; then
|
||||||
|
error_log "Automatisches Deployment fehlgeschlagen beim Bauen des Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Keine Image-Datei gefunden. Baue neues Image..."
|
||||||
|
if ! build_image; then
|
||||||
|
error_log "Automatisches Deployment fehlgeschlagen beim Bauen des Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Speichere das Image für zukünftige Verwendung
|
||||||
|
log "Speichere Image für zukünftige Verwendung..."
|
||||||
|
save_image
|
||||||
|
|
||||||
|
# Starte den Container
|
||||||
|
log "Starte Container..."
|
||||||
|
if ! start_container_compose; then
|
||||||
|
error_log "Konnte Container nicht mit Docker Compose starten. Versuche direkten Start..."
|
||||||
|
if ! start_container_run; then
|
||||||
|
error_log "Automatisches Deployment fehlgeschlagen beim Starten des Containers."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Automatisches Deployment erfolgreich abgeschlossen!"
|
||||||
|
log "Frontend ist unter http://localhost:3000 erreichbar"
|
||||||
|
log "API-Kommunikation mit Backend: $BACKEND_URL"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hauptanwendung
|
||||||
|
# Zuerst nach Backend-URL fragen
|
||||||
|
header "Backend-URL Konfiguration"
|
||||||
|
log "Standard-Backend-URL: $DEFAULT_BACKEND_URL"
|
||||||
|
read -p "Möchten Sie eine andere Backend-URL verwenden? (j/n): " change_url_choice
|
||||||
|
|
||||||
|
if [[ "$change_url_choice" == "j" ]]; then
|
||||||
|
read -p "Geben Sie die neue Backend-URL ein (z.B. http://192.168.0.105:5000): " custom_url
|
||||||
|
if [ -n "$custom_url" ]; then
|
||||||
|
BACKEND_URL="$custom_url"
|
||||||
|
log "Verwende benutzerdefinierte Backend-URL: $BACKEND_URL"
|
||||||
|
else
|
||||||
|
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
||||||
|
log "Leere Eingabe. Verwende Standard-Backend-URL: $BACKEND_URL"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
||||||
|
log "Verwende Standard-Backend-URL: $BACKEND_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Anzeigen des Hauptmenüs
|
||||||
|
main_menu
|
Loading…
x
Reference in New Issue
Block a user