Files
drivetimeplaner/docs/plans/2026-05-20-slim-api-improvements.md
Hermes Agent 09759b7619 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
2026-05-20 17:30:24 +02:00

9.1 KiB

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

echo 'JWT_SECRET=your-256-bit-secret-change-in-production' > api/.env

Schritt 2: Container.php anpassen

Ersetze in Container.php Zeile 45:

'jwt.secret' => 'your-256-bit-secret-change-in-production',

mit:

'jwt.secret' => $_ENV['JWT_SECRET'] ?? throw new \RuntimeException('JWT_SECRET not set'),

Schritt 3: Testen

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

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

openssl rand -base64 32

Schritt 2: Secret in Git encrypted speichern

# 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

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

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

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
// 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

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

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

// 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

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

# 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)

Schritt 3: Commit

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

# DriveTime Planner API

## Base URL
`https://api.fahrschultermin.de/api/v1`

## Authentication
JWT Bearer Token

Authorization: Bearer


## 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

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

#!/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

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