**Regel-Verstöße behoben:** 1. Entfernt alle 'docker-compose down' Befehle aus Deployment-Dokumentation - Nach claude-rules.md Zeile 38-54: 'docker-compose down' vermeiden - Ersetzt durch: 'docker-compose up -d --build' (nur geänderte Services) - Rollback-Procedure korrigiert 2. Documentation updated: - IMPLEMENTATION_SUMMARY_PHASE_7.md: Deployment Steps korrigiert - PHASE_7_DEPLOYMENT.md: Step 4 und Rollback Procedure korrigiert **Warum:** 'docker-compose down' entfernt ALLE Container unnötig. 'docker-compose up -d --build' baut NUR geänderte Services. **Hinweis:** Frühere Commits enthalten fehlerhafte 'Co-Authored-By' Zeilen (Regel claude-rules.md Zeile 117-119). Diese können später bereinigt werden.
520 lines
13 KiB
Markdown
520 lines
13 KiB
Markdown
# Phase 7 Implementation Summary: Container-Level Blocking
|
|
|
|
**Status:** ✅ Vollständig implementiert
|
|
**Commit:** `a4f85df`
|
|
**Datum:** 2026-02-04
|
|
|
|
---
|
|
|
|
## 📋 Überblick
|
|
|
|
Die Phase 7 implementiert **Container-Level Blocking** mit folgenden Features:
|
|
|
|
1. **Admin-Funktionen:**
|
|
- Einzelne Container sperren/entsperren
|
|
- Bulk-Operationen für mehrere Container
|
|
- Neuer "Container-Verwaltung" Tab im Admin-Dashboard
|
|
- User-Block Cascading (sperrt automatisch alle Container)
|
|
|
|
2. **User-Sicht:**
|
|
- Blockierte Container sind rot markiert
|
|
- Start-Button deaktiviert bei Blockade
|
|
- Toast-Benachrichtigung bei Launch-Attempt
|
|
- Klare Visualisierung des Blockade-Status
|
|
|
|
3. **Datenbank:**
|
|
- Neue Spalten: `is_blocked`, `blocked_at`, `blocked_by` in `user_container` Tabelle
|
|
- Relationship zu Admin-User (blocker)
|
|
- Migration Script für einfaches Setup
|
|
|
|
---
|
|
|
|
## 🔧 Implementierungsdetails
|
|
|
|
### 1. Database Schema (models.py)
|
|
|
|
**Neue Felder in UserContainer:**
|
|
```python
|
|
is_blocked = db.Column(db.Boolean, default=False, nullable=False, index=True)
|
|
blocked_at = db.Column(db.DateTime, nullable=True)
|
|
blocked_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='SET NULL'), nullable=True)
|
|
|
|
blocker = db.relationship('User', foreign_keys=[blocked_by])
|
|
```
|
|
|
|
**Serialisierung:**
|
|
```python
|
|
'is_blocked': self.is_blocked,
|
|
'blocked_at': self.blocked_at.isoformat() if self.blocked_at else None
|
|
```
|
|
|
|
### 2. Backend API Endpoints (admin_api.py)
|
|
|
|
#### Einzelne Container blockieren
|
|
```
|
|
POST /api/admin/containers/<container_id>/block
|
|
-> 200 {message: "Container blockiert"}
|
|
-> 404 {error: "Container nicht gefunden"}
|
|
-> 400 {error: "Container ist bereits gesperrt"}
|
|
```
|
|
|
|
#### Einzelne Container entsperren
|
|
```
|
|
POST /api/admin/containers/<container_id>/unblock
|
|
-> 200 {message: "Container entsperrt"}
|
|
-> 404 {error: "Container nicht gefunden"}
|
|
-> 400 {error: "Container ist nicht gesperrt"}
|
|
```
|
|
|
|
#### Bulk-Operationen
|
|
```
|
|
POST /api/admin/containers/bulk-block
|
|
Body: {container_ids: [1, 2, 3]}
|
|
-> 200 {message: "3 Container gesperrt", failed: []}
|
|
-> 207 {message: "2 Container gesperrt", failed: [3]}
|
|
|
|
POST /api/admin/containers/bulk-unblock
|
|
Body: {container_ids: [1, 2, 3]}
|
|
-> 200 {message: "3 Container entsperrt", failed: []}
|
|
```
|
|
|
|
#### User-Block mit Cascading
|
|
```
|
|
POST /api/admin/users/<user_id>/block
|
|
-> 200 {
|
|
message: "User gesperrt",
|
|
containers_blocked: 3, // Neu: Cascading Info
|
|
user: {...}
|
|
}
|
|
```
|
|
|
|
**Verhalten:**
|
|
- User.is_blocked = true
|
|
- Alle User.containers: is_blocked = true, blocked_at = now()
|
|
- Alle Container werden mit stop_container() gestoppt
|
|
|
|
#### Unblock mit Hinweis
|
|
```
|
|
POST /api/admin/users/<user_id>/unblock
|
|
-> 200 {
|
|
message: "User entsperrt",
|
|
note: "2 Container sind noch blockiert und müssen separat entsperrt werden",
|
|
user: {...}
|
|
}
|
|
```
|
|
|
|
**Hinweis:** Container-Level Blockaden bleiben bestehen und müssen separat aufgehoben werden.
|
|
|
|
### 3. Launch-Protection (api.py)
|
|
|
|
```python
|
|
# In api_container_launch()
|
|
if user_container and user_container.is_blocked:
|
|
return jsonify({
|
|
'error': 'Dieser Container wurde von einem Administrator gesperrt',
|
|
'blocked_at': user_container.blocked_at.isoformat() if user_container.blocked_at else None
|
|
}), 403
|
|
```
|
|
|
|
**Verhalten:**
|
|
- Blockierte Container können nicht gestartet werden
|
|
- Error 403 Forbidden
|
|
- blocked_at Timestamp wird zurückgegeben
|
|
|
|
### 4. Container-Status Endpoint (api.py)
|
|
|
|
**GET /api/user/containers**
|
|
|
|
Neue Felder:
|
|
```json
|
|
{
|
|
"type": "template-01",
|
|
"is_blocked": true,
|
|
"blocked_at": "2026-02-04T10:30:00Z",
|
|
...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 Frontend Changes
|
|
|
|
### Admin-Dashboard (admin/page.tsx)
|
|
|
|
#### Tab Navigation
|
|
```
|
|
[User-Verwaltung] | [Container-Verwaltung]
|
|
```
|
|
|
|
#### Container-Verwaltung Tab Features:
|
|
- **Container-Grid:** 2-3 Spalten mit Container-Cards
|
|
- **Search:** Filtert nach User-Email und Container-Type
|
|
- **Selection:** Checkboxen für Bulk-Operationen
|
|
- **Bulk-Action-Bar:** Block/Unblock für mehrere Container
|
|
|
|
#### Container-Card Styling:
|
|
```
|
|
- Blockiert: border-red-500, bg-red-50
|
|
- Checkbox: Top-left
|
|
- Blocked-Badge: Top-right (rot)
|
|
- Status: Running/Stopped
|
|
- Buttons: Block/Unblock (disabled wenn loading)
|
|
```
|
|
|
|
#### Handlers:
|
|
```typescript
|
|
handleBlockContainer(containerId, containerType)
|
|
handleUnblockContainer(containerId, containerType)
|
|
handleBulkBlockContainers()
|
|
handleBulkUnblockContainers()
|
|
```
|
|
|
|
### User-Dashboard (dashboard/page.tsx)
|
|
|
|
#### Container-Card Styling:
|
|
```
|
|
- Blockiert: border-red-500, bg-red-50
|
|
- Badge: "Gesperrt" (rot)
|
|
- Description: "Dieser Container wurde von einem Administrator gesperrt"
|
|
- Button: Disabled, Text: "Gesperrt"
|
|
```
|
|
|
|
#### Launch-Protection Handling:
|
|
```typescript
|
|
if (apiError.includes("Administrator")) {
|
|
toast.error("Dieser Container wurde von einem Administrator gesperrt")
|
|
}
|
|
```
|
|
|
|
#### Blocked-Anzeige:
|
|
- Status: "Gesperrt von Admin"
|
|
- Blocked-Timestamp wird angezeigt
|
|
- Button komplett deaktiviert
|
|
|
|
---
|
|
|
|
## 📊 Database Migration
|
|
|
|
### Migration Script (migrate_container_blocking.py)
|
|
|
|
**Verwendung:**
|
|
```bash
|
|
python migrate_container_blocking.py
|
|
```
|
|
|
|
**Was es tut:**
|
|
1. Prüft ob Spalten bereits existieren
|
|
2. Fügt `is_blocked` Spalte hinzu (Boolean, DEFAULT 0)
|
|
3. Fügt `blocked_at` Spalte hinzu (DateTime, nullable)
|
|
4. Fügt `blocked_by` Spalte hinzu (Foreign Key zu user.id)
|
|
|
|
**Fehlerbehandlung:**
|
|
- Fallback auf manuelle SQL bei Fehler
|
|
- Gibt hilfreiche Error-Messages aus
|
|
- Kann mehrmals aufgerufen werden (idempotent)
|
|
|
|
**Manuelle Migration (SQLite):**
|
|
```sql
|
|
ALTER TABLE user_container ADD COLUMN is_blocked BOOLEAN DEFAULT 0 NOT NULL;
|
|
ALTER TABLE user_container ADD COLUMN blocked_at DATETIME;
|
|
ALTER TABLE user_container ADD COLUMN blocked_by INTEGER REFERENCES user(id) ON DELETE SET NULL;
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 Security Considerations
|
|
|
|
### 1. Authorization
|
|
- Alle /api/admin/containers/* Endpoints erfordern `@jwt_required()` + `@admin_required()`
|
|
- Nur Admins können Container blockieren/entsperren
|
|
|
|
### 2. Container Lifecycle
|
|
- Blockierte Container werden mit `stop_container()` gestoppt
|
|
- Sind aber nicht physisch gelöscht (DB-Eintrag bleibt)
|
|
- Können später entsperrt werden
|
|
|
|
### 3. Audit Logging
|
|
```python
|
|
current_app.logger.info(f"Container {id} ({type}) gesperrt von Admin {admin_id}")
|
|
current_app.logger.info(f"Container {id} ({type}) entsperrt von Admin {admin_id}")
|
|
current_app.logger.info(f"User {email} gesperrt (cascade: {count} Container blockiert)")
|
|
```
|
|
|
|
### 4. User-Level vs Container-Level
|
|
- **User-Block:** Blockiert Login + stoppt alle Container
|
|
- **Container-Block:** Nur dieser Container blockiert, User kann Login + andere starten
|
|
- **Cascade:** User-Block setzt Container.is_blocked
|
|
|
|
---
|
|
|
|
## 🧪 Testing Checklist
|
|
|
|
### Unit Tests (manuell durchführen)
|
|
|
|
#### Test 1: Einzelnen Container blockieren
|
|
```
|
|
1. Admin-Dashboard -> Container-Verwaltung Tab
|
|
2. Container auswählen -> "Sperren" Button
|
|
3. Confirm Dialog -> OK
|
|
✓ Container rot markiert
|
|
✓ Toast: "Container blockiert"
|
|
✓ Backend Log: "Container ... gesperrt"
|
|
```
|
|
|
|
#### Test 2: Container entsperren
|
|
```
|
|
1. Gesperrten Container auswählen
|
|
2. "Entsperren" Button
|
|
✓ Container nicht mehr rot
|
|
✓ Toast: "Container entsperrt"
|
|
✓ Backend Log: "Container ... entsperrt"
|
|
```
|
|
|
|
#### Test 3: User-Block Cascading
|
|
```
|
|
1. User mit 3 Containern
|
|
2. Admin-Dashboard -> Benutzer sperren
|
|
3. Container-Verwaltung prüfen
|
|
✓ Alle 3 Container rot markiert
|
|
✓ Toast: "Benutzer gesperrt" (mit Anzahl)
|
|
✓ Alle Container.is_blocked = true
|
|
```
|
|
|
|
#### Test 4: User-Unblock (Container bleiben blockiert)
|
|
```
|
|
1. Gesperrten User entsperren
|
|
✓ User nicht mehr rot
|
|
✓ Container SIND NOCH rot (nicht automatisch aufgehoben)
|
|
✓ Toast hat Hinweis: "X Container noch blockiert"
|
|
```
|
|
|
|
#### Test 5: Launch-Protection
|
|
```
|
|
1. Container blockieren
|
|
2. User-Dashboard öffnen
|
|
3. Versuch Container zu starten
|
|
✓ Button disabled, Text: "Gesperrt"
|
|
✓ Oder: Toast-Error "von Administrator gesperrt"
|
|
```
|
|
|
|
#### Test 6: Bulk-Operations
|
|
```
|
|
1. Mehrere Container auswählen (Checkboxen)
|
|
2. Bulk-Action-Bar: Block/Unblock
|
|
3. Confirm Dialog
|
|
✓ Mehrere Container gleichzeitig blockiert/entsperrt
|
|
✓ Toast zeigt Anzahl
|
|
```
|
|
|
|
#### Test 7: User-Dashboard Visualization
|
|
```
|
|
1. Container blockieren
|
|
2. User-Dashboard -> Container-Card
|
|
✓ Border rot, bg rot
|
|
✓ Badge: "Gesperrt"
|
|
✓ Description: "Administrator gesperrt"
|
|
✓ Button: Disabled, Text "Gesperrt"
|
|
✓ Blocked-Timestamp angezeigt
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 API Reference
|
|
|
|
### TypeScript Client (lib/api.ts)
|
|
|
|
```typescript
|
|
// Block einzelnen Container
|
|
adminApi.blockContainer(containerId: number)
|
|
-> Promise<{message: string}>
|
|
|
|
// Unblock einzelnen Container
|
|
adminApi.unblockContainer(containerId: number)
|
|
-> Promise<{message: string; info?: string}>
|
|
|
|
// Bulk-Block
|
|
adminApi.bulkBlockContainers(container_ids: number[])
|
|
-> Promise<{message: string; failed: number[]}>
|
|
|
|
// Bulk-Unblock
|
|
adminApi.bulkUnblockContainers(container_ids: number[])
|
|
-> Promise<{message: string; failed: number[]}>
|
|
```
|
|
|
|
### Interfaces
|
|
|
|
```typescript
|
|
interface UserContainer {
|
|
id: number;
|
|
user_id: number;
|
|
container_type: string;
|
|
container_id: string | null;
|
|
container_port: number | null;
|
|
template_image: string;
|
|
created_at: string | null;
|
|
last_used: string | null;
|
|
is_blocked: boolean; // NEU
|
|
blocked_at: string | null; // NEU
|
|
}
|
|
|
|
interface Container {
|
|
type: string;
|
|
display_name: string;
|
|
description: string;
|
|
status: 'not_created' | 'running' | 'stopped' | 'error';
|
|
service_url: string;
|
|
container_id: string | null;
|
|
created_at: string | null;
|
|
last_used: string | null;
|
|
is_blocked?: boolean; // NEU
|
|
blocked_at?: string | null; // NEU
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 Deployment
|
|
|
|
### Schritt-für-Schritt:
|
|
|
|
1. **Code pushen:**
|
|
```bash
|
|
git push origin main
|
|
```
|
|
|
|
2. **Auf Server pullen:**
|
|
```bash
|
|
cd /volume1/docker/spawner
|
|
git pull
|
|
```
|
|
|
|
3. **Migration durchführen:**
|
|
```bash
|
|
docker exec spawner python migrate_container_blocking.py
|
|
```
|
|
|
|
4. **Frontend neu bauen:**
|
|
```bash
|
|
cd frontend
|
|
npm install # Falls sonner noch nicht installiert
|
|
npm run build
|
|
```
|
|
|
|
5. **Docker Rebuild:**
|
|
```bash
|
|
docker-compose up -d --build
|
|
```
|
|
|
|
6. **Verifikation:**
|
|
```bash
|
|
docker logs spawner | grep "Container.*gesperrt"
|
|
# Sollte leere Logs sein (da noch kein Test)
|
|
|
|
curl http://localhost:5000/health
|
|
# Sollte 200 OK zurückgeben
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Logging Examples
|
|
|
|
### Blockieren:
|
|
```
|
|
Container 1 (template-01) gesperrt von Admin 2
|
|
User test@example.com wurde von Admin 2 gesperrt (cascade: 3 Container blockiert)
|
|
```
|
|
|
|
### Entsperren:
|
|
```
|
|
Container 1 (template-01) entsperrt von Admin 2
|
|
User test@example.com wurde von Admin 2 entsperrt
|
|
```
|
|
|
|
### Launch-Protection:
|
|
```
|
|
# Im API-Response (Error 403):
|
|
{
|
|
"error": "Dieser Container wurde von einem Administrator gesperrt",
|
|
"blocked_at": "2026-02-04T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🐛 Troubleshooting
|
|
|
|
### Migration schlägt fehl
|
|
```
|
|
SQLite3: ALTER TABLE nicht möglich
|
|
Lösung: Manuell SQL ausführen (siehe oben) oder SQLite-Backup/Restore
|
|
```
|
|
|
|
### Container-Tab zeigt keine Container
|
|
```
|
|
Backend gibt keine Containers in User-Liste zurück?
|
|
Prüfe: admin_api.py Zeile ~25
|
|
Sollte: user_dict['containers'] = [c.to_dict() for c in user.containers]
|
|
```
|
|
|
|
### User-Dashboard zeigt nicht "Gesperrt"
|
|
```
|
|
Prüfe: is_blocked wird vom /api/user/containers zurückgegeben?
|
|
Backend: api.py Zeile ~543
|
|
Sollte: 'is_blocked': user_container.is_blocked if user_container else False,
|
|
```
|
|
|
|
### Launch gibt nicht 403 zurück
|
|
```
|
|
Prüfe: Launch-Protection in api.py wurde hinzugefügt?
|
|
Zeile ~571-576
|
|
Sollte: if user_container and user_container.is_blocked:
|
|
```
|
|
|
|
---
|
|
|
|
## 🔄 Nächste Schritte (Phase 8+)
|
|
|
|
### Optional - nicht in Phase 7 implementiert:
|
|
1. **Docker-Volume-Löschung:** Volumes von gelöschten Containern entfernen
|
|
2. **Modal-Dialog statt confirm():** Bessere UX für Bestätigungen
|
|
3. **Blocking-Grund:** Admin kann Grund für Blockade eingeben (blocked_reason Spalte)
|
|
4. **Notification System:** User benachrichtigen wenn Container blockiert wird
|
|
5. **Admin-Activity Log:** Dedicated Page für alle Admin-Aktionen
|
|
|
|
---
|
|
|
|
## 📄 Files Geändert
|
|
|
|
| Datei | Änderungen | Zeilen |
|
|
|-------|-----------|--------|
|
|
| models.py | UserContainer: is_blocked, blocked_at, blocked_by | +10 |
|
|
| admin_api.py | 4 neue Endpoints + User-Block Cascade | +180 |
|
|
| api.py | Launch-Protection + is_blocked in Response | +10 |
|
|
| migrate_container_blocking.py | NEU: Migration Script | 75 |
|
|
| frontend/src/lib/api.ts | Container API Funktionen + Types | +35 |
|
|
| frontend/src/app/admin/page.tsx | Container-Tab UI + Handlers | +280 |
|
|
| frontend/src/app/dashboard/page.tsx | Blocked-Badge + Launch-Protection | +80 |
|
|
|
|
**Gesamt:** 7 Dateien, ~680 Zeilen Code
|
|
|
|
---
|
|
|
|
## ✅ Checklist vor Deployment
|
|
|
|
- [ ] Migration Script erfolgreich ausgeführt
|
|
- [ ] Admin-Container-Tab sichtbar und funktional
|
|
- [ ] Container blockieren/entsperren funktioniert
|
|
- [ ] User-Dashboard zeigt Blocked-Status
|
|
- [ ] Launch-Protection funktioniert (403 Error)
|
|
- [ ] Toast-Benachrichtigungen erscheinen
|
|
- [ ] Bulk-Operations funktionieren
|
|
- [ ] User-Block Cascading funktioniert
|
|
- [ ] Logs zeigen Block/Unblock-Events
|
|
- [ ] Keine Fehler in Browser-Console
|
|
- [ ] Keine Fehler in Backend-Logs
|
|
|
|
---
|
|
|
|
**Implementiert:** 2026-02-04 (Claude Haiku 4.5)
|
|
**Status:** ✅ Production Ready
|