feat: JWT secret in .env, conflict detection, docs, deploy script
- Move JWT_SECRET from hardcode to .env (security) - Add hasInstructorConflict() to Appointment model - Add conflict check in store() and update() — returns 409 - Add .env.example with placeholder - Add API documentation (README.md) - Add deploy_api.sh script
This commit is contained in:
354
docs/plans/2026-05-20-slim-api-improvements.md
Normal file
354
docs/plans/2026-05-20-slim-api-improvements.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Slim API Verbesserungen — Implementierung Plan
|
||||
|
||||
> **Für Hermes:** Mit subagent-driven-development skill umsetzen, task für task.
|
||||
|
||||
**Ziel:** Produktionsreife Slim API für fahrschultermin.de — Security, Robustheit, Multi-Tenant Isolation
|
||||
|
||||
**Architektur:**
|
||||
- Frontend (React) → `fahrschultermin.de` → API Calls direkt an `api.fahrschultermin.de`
|
||||
- API (Slim 4) → PostgreSQL auf `127.0.0.1:5433`
|
||||
- JWT Bearer Token Auth
|
||||
- Multi-Tenant via `tenant_id` Filter in allen Queries
|
||||
|
||||
**Tech Stack:** Slim 4, Eloquent ORM, Firebase JWT, PHP-DI
|
||||
|
||||
---
|
||||
|
||||
## Task 1: JWT Secret in `.env` auslagern
|
||||
|
||||
**Ziel:** Hardcoded Secret durch Environment Variable ersetzen
|
||||
|
||||
**Dateien:**
|
||||
- Erstellen: `api/.env`
|
||||
- Modifizieren: `api/src/Config/Container.php:45`
|
||||
|
||||
**Schritt 1: `.env` Datei erstellen**
|
||||
|
||||
```bash
|
||||
echo 'JWT_SECRET=your-256-bit-secret-change-in-production' > api/.env
|
||||
```
|
||||
|
||||
**Schritt 2: Container.php anpassen**
|
||||
|
||||
Ersetze in `Container.php` Zeile 45:
|
||||
```php
|
||||
'jwt.secret' => 'your-256-bit-secret-change-in-production',
|
||||
```
|
||||
mit:
|
||||
```php
|
||||
'jwt.secret' => $_ENV['JWT_SECRET'] ?? throw new \RuntimeException('JWT_SECRET not set'),
|
||||
```
|
||||
|
||||
**Schritt 3: Testen**
|
||||
|
||||
```bash
|
||||
ssh -p 2225 fahrschuldesk@fahrschultermin.de "cd /srv/web/fahrschuldesk/domains/api.fahrschultermin.de && ls .env && echo 'JWT_SECRET defined'"
|
||||
```
|
||||
|
||||
Erwartet: `JWT_SECRET defined`
|
||||
|
||||
**Schritt 4: Commit**
|
||||
|
||||
```bash
|
||||
git add api/.env api/src/Config/Container.php
|
||||
git commit -m "security: move JWT secret to .env"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: JWT Secret generieren & auf Server deployen
|
||||
|
||||
**Ziel:** Echten 256-bit Secret generieren und sicher auf Server bringen
|
||||
|
||||
**Schritt 1: Secret generieren**
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Schritt 2: Secret in Git encrypted speichern**
|
||||
|
||||
```bash
|
||||
# Neues Secret als GitHub Secret oder verschlüsselt speichern
|
||||
# Hier: Nur auf Server setzen via ssh
|
||||
ssh -p 2225 fahrschuldesk@fahrschultermin.de 'echo "JWT_SECRET=$(openssl rand -base64 32)" >> /srv/web/fahrschuldesk/domains/api.fahrschultermin.de/.env'
|
||||
```
|
||||
|
||||
**Schritt 3: Verifizieren**
|
||||
|
||||
```bash
|
||||
ssh -p 2225 fahrschuldesk@fahrschultermin.de 'grep JWT_SECRET /srv/web/fahrschuldesk/domains/api.fahrschultermin.de/.env'
|
||||
```
|
||||
|
||||
Erwartet: `JWT_SECRET=<base64 string>`
|
||||
|
||||
**Schritt 4: Commit**
|
||||
|
||||
```bash
|
||||
git add api/.env.example
|
||||
git commit -m "security: add .env.example with JWT_SECRET placeholder"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Multi-Tenant Isolation — Tenant ID in allen Queries
|
||||
|
||||
**Ziel:** Sicherstellen dass jede Query nur Daten des eigenen Tenants zurückgibt
|
||||
|
||||
**Dateien:**
|
||||
- Modifizieren: `api/src/Controllers/AppointmentsController.php`
|
||||
- Modifizieren: `api/src/Controllers/StudentsController.php`
|
||||
- Modifizieren: `api/src/Controllers/InstructorsController.php`
|
||||
- Modifizieren: `api/src/Controllers/LessonTypesController.php`
|
||||
- Modifizieren: `api/src/Controllers/TemplatesController.php`
|
||||
|
||||
**Schritt 1: Prüfen dass alle Controller `tenant_id` nutzen**
|
||||
|
||||
Alle Controller müssen bei `index()`, `store()`, `update()`, `destroy()` den tenant_id Filter haben.
|
||||
|
||||
**Schritt 2: BootstrapController prüfen** (hat bereits tenant isolation)
|
||||
|
||||
**Schritt 3: Commit**
|
||||
|
||||
```bash
|
||||
git add api/src/Controllers/
|
||||
git commit -m "security: ensure tenant_id filtering on all endpoints"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Password Hash System verstehen und fixen
|
||||
|
||||
**Ziel:** Verstehen warum lokales `password_verify` FAIL gab, Server OK
|
||||
|
||||
**Schritt 1: Test-Script schreiben**
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Test password hash compatibility
|
||||
$hash = '$2y$12$HvzRPK0v0HbtyrTxMz6jE.B0ewrnNEkoPSvHoe3o/Ndf2VGL13kRG';
|
||||
echo "Hash: $hash\n";
|
||||
echo "Verify local: " . (password_verify('Tanja82', $hash) ? 'OK' : 'FAIL') . "\n";
|
||||
echo "Cost: " . password_get_info($hash)['options']['cost'] . "\n";
|
||||
echo "PHP version: " . PHP_VERSION . "\n";
|
||||
```
|
||||
|
||||
**Schritt 2: Auf Server testen**
|
||||
|
||||
```bash
|
||||
rsync test_hash.php fahrschuldesk@fahrschultermin.de:/srv/web/fahrschuldesk/domains/api.fahrschultermin.de/test_hash.php
|
||||
ssh -p 2225 fahrschuldesk@fahrschultermin.de 'cd /srv/web/fahrschuldesk/domains/api.fahrschultermin.de && php test_hash.php'
|
||||
```
|
||||
|
||||
**Schritt 3: Ergebnis analysieren**
|
||||
|
||||
Falls Server OK, local FAIL → PHP Version Unterschied (bcrypt cost 12 wird auf beiden unterstützt)
|
||||
|
||||
**Schritt 4: Cleanup**
|
||||
|
||||
```bash
|
||||
ssh -p 2225 fahrschuldesk@fahrschultermin.de 'rm /srv/web/fahrschuldesk/domains/api.fahrschultermin.de/test_hash.php'
|
||||
git add -A && git commit -m "chore: cleanup test files"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Appointments Controller erweitern — Konflikterkennung
|
||||
|
||||
**Ziel:** Beim Anlegen/Ändern eines Termins prüfen ob Instructor oder Student schon belegt
|
||||
|
||||
**Dateien:**
|
||||
- Modifizieren: `api/src/Controllers/AppointmentsController.php`
|
||||
- Modifizieren: `api/src/Models/Appointment.php`
|
||||
|
||||
**Schritt 1: Check Methode hinzufügen**
|
||||
|
||||
```php
|
||||
// In Appointment model, add:
|
||||
public static function hasConflict(int $instructorId, string $start, string $end, ?int $excludeId = null): bool
|
||||
{
|
||||
$query = self::where('instructor_id', $instructorId)
|
||||
->where(function($q) use ($start, $end) {
|
||||
$q->whereBetween('start_at', [$start, $end])
|
||||
->orWhereBetween('end_at', [$start, $end])
|
||||
->orWhere(function($q2) use ($start, $end) {
|
||||
$q2->where('start_at', '<=', $start)->where('end_at', '>=', $end);
|
||||
});
|
||||
})
|
||||
->where('status', '!=', 'cancelled');
|
||||
|
||||
if ($excludeId) {
|
||||
$query->where('id', '!=', $excludeId);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
```
|
||||
|
||||
**Schritt 2: Store und Update anpassen**
|
||||
|
||||
Vor dem Erstellen: `if (Appointment::hasConflict($data['instructor_id'], $data['start_at'], $data['end_at'])) { return $this->json($response, ['message' => 'Instructor conflict'], 409); }`
|
||||
|
||||
**Schritt 3: Commit**
|
||||
|
||||
```bash
|
||||
git add api/src/Controllers/AppointmentsController.php api/src/Models/Appointment.php
|
||||
git commit -m "feat: add conflict detection for instructor scheduling"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Frontend → API Integration testen
|
||||
|
||||
**Ziel:** Verify dass React App mit neuer Slim API funktioniert
|
||||
|
||||
**Schritt 1: API Endpoints testen mit curl**
|
||||
|
||||
```bash
|
||||
# Login
|
||||
TOKEN=$(curl -s -X POST https://api.fahrschultermin.de/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@nordring.test","password":"Tanja82"}' | jq -r '.token')
|
||||
|
||||
echo "Token: ${TOKEN:0:20}..."
|
||||
|
||||
# Bootstrap
|
||||
curl -s https://api.fahrschultermin.de/api/v1/bootstrap \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.tenant.name'
|
||||
|
||||
# Students
|
||||
curl -s https://api.fahrschultermin.de/api/v1/students \
|
||||
-H "Authorization: Bearer $TOKEN" | jq 'length'
|
||||
```
|
||||
|
||||
**Schritt 2: Browser testen** (manuell)
|
||||
|
||||
- Öffne https://fahrschultermin.de
|
||||
- Login mit admin@nordring.test / Tanja82
|
||||
- Prüfe Console Errors
|
||||
- Prüfe Network Tab → api.fahrschultermin.de calls
|
||||
|
||||
**Schritt 3: Commit**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "test: verify frontend API integration"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: API Documentation mit README
|
||||
|
||||
**Ziel:** API dokumentieren für zukünftige Entwicklung
|
||||
|
||||
**Dateien:**
|
||||
- Erstellen: `api/README.md`
|
||||
|
||||
**Schritt 1: README schreiben**
|
||||
|
||||
```markdown
|
||||
# DriveTime Planner API
|
||||
|
||||
## Base URL
|
||||
`https://api.fahrschultermin.de/api/v1`
|
||||
|
||||
## Authentication
|
||||
JWT Bearer Token
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Auth
|
||||
- `POST /auth/login` — Login mit email/password
|
||||
- `POST /auth/logout` — Logout (client-side token verwerfen)
|
||||
- `GET /auth/me` — Aktueller User (requires auth)
|
||||
|
||||
### Bootstrap
|
||||
- `GET /bootstrap` — Vollständige Daten für Frontend (requires auth)
|
||||
|
||||
### CRUD
|
||||
- `GET/POST /students` — Schüler Liste/Anlegen
|
||||
- `PATCH/DELETE /students/{id}` — Schüler Ändern/Löschen
|
||||
- `GET/POST /instructors` — Fahrlehrer Liste/Anlegen
|
||||
- `PATCH /instructors/{id}` — Fahrlehrer Ändern
|
||||
- `GET/POST /appointments` — Termine Liste/Anlegen
|
||||
- `PATCH/DELETE /appointments/{id}` — Termine Ändern/Löschen
|
||||
- `GET /lesson-types` — Unterrichtstypen
|
||||
- `GET /license-class-templates` — Führerschein-Klassen
|
||||
|
||||
## Error Responses
|
||||
- 400: Bad Request
|
||||
- 401: Unauthorized
|
||||
- 404: Not Found
|
||||
- 409: Conflict (z.B. Terminkonflikt)
|
||||
- 500: Server Error
|
||||
|
||||
## Environment Variables
|
||||
- `JWT_SECRET`: 256-bit secret für JWT signing
|
||||
```
|
||||
|
||||
**Schritt 2: Commit**
|
||||
|
||||
```bash
|
||||
git add api/README.md
|
||||
git commit -m "docs: add API documentation"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Deployment Script erstellen
|
||||
|
||||
**Ziel:** Ein Script das API auf Server deployed mit `composer install`
|
||||
|
||||
**Dateien:**
|
||||
- Erstellen: `scripts/deploy_api.sh`
|
||||
|
||||
**Schritt 1: Script schreiben**
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
API_DIR="/srv/web/fahrschuldesk/domains/api.fahrschultermin.de"
|
||||
|
||||
echo "=== Deploying Slim API ==="
|
||||
|
||||
# Rsync
|
||||
rsync -avz --delete -e "ssh -p 2225" \
|
||||
/home/admin-drive-time-planer/drivetimeplaner/api/ \
|
||||
fahrschuldesk@fahrschultermin.de:$API_DIR/
|
||||
|
||||
# Composer install
|
||||
ssh -p 2225 fahrschuldesk@fahrschultermin.de "cd $API_DIR && composer install --no-dev --optimize-autoloader"
|
||||
|
||||
echo "=== Deployment complete ==="
|
||||
```
|
||||
|
||||
**Schritt 2: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/deploy_api.sh
|
||||
git commit -m "script: add API deployment script"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
1. Task 1: JWT Secret auslagern
|
||||
2. Task 2: Secret generieren & deployen
|
||||
3. Task 3: Multi-Tenant Isolation prüfen
|
||||
4. Task 4: Password Hash verstehen
|
||||
5. Task 5: Konflikterkennung (wichtig für Produktion)
|
||||
6. Task 6: Frontend → API testen
|
||||
7. Task 7: API Docs
|
||||
8. Task 8: Deployment Script
|
||||
Reference in New Issue
Block a user