Zum Hauptinhalt springen
Schneespur
Dokumentation durchblättern

Module entwickeln

Routen & APIs

Wie ein Modul eigene Web-Routen, Portal- und Fahrer-Seiten sowie token-authentifizierte API-Endpunkte registriert — mit Namenskonventionen, Middleware und Beispielen.

Ein Modul, das eigene Oberflächen oder Schnittstellen anbietet, braucht eigene Routen. Schneespur baut hier auf dem Standard-Routing von Laravel auf — es gibt keine modul-eigene Routing-Sprache, die Sie zusätzlich lernen müssten. Diese Seite zeigt, wie Sie Web-Routen für Admin-, Portal- und Fahrer-Bereiche registrieren und wie Sie über den ModuleApiRegistrar token-geschützte API-Endpunkte bereitstellen.

Web-Routen registrieren

Web-Routen meldet ein Modul in der boot()-Methode seines ServiceProviders an. Das ist bewusst dieselbe Stelle, an der auch Navigation, Widgets und Einstellungen registriert werden — alle Erweiterungspunkte eines Moduls liegen an einem Ort.

protected function registerRoutes(): void
{
    Route::middleware(['web', 'auth'])
        ->prefix('admin/my-module')
        ->name('admin.my-module.')
        ->group(function () {
            Route::get('settings', [MySettingsController::class, 'index'])
                ->name('settings');
            Route::post('settings', [MySettingsController::class, 'update'])
                ->name('settings.update');
        });
}

Namenskonvention für Routen

Routennamen folgen einem festen Muster. Der Grund: Andere Teile der Anwendung und andere Module verweisen über route('admin.my-module.settings') auf Ihre Seiten. Halten Sie sich an das Muster, bleiben diese Verweise vorhersehbar und kollidieren nicht.

MusterBeispiel
Admin-Seitenadmin.{module-slug}.{action}
Admin-Ressourceadmin.{module-slug}.{resource}.{action}

Der {slug} im Routennamen ist immer der Modul-Slug — derselbe Bezeichner, unter dem auch Einstellungen und API-Routen laufen. Das macht Routen einem Modul eindeutig zuordenbar.

Middleware

Middleware entscheidet, wer eine Route überhaupt erreicht. Wählen Sie sie nach der Zielgruppe der Route aus, nicht nach Gewohnheit — eine Fahrer-Route mit reiner auth-Middleware wäre auch für Admins und Kunden offen.

MiddlewareZweck
webSession, CSRF, Cookies
authnur angemeldete Nutzer
auth:customerGuard für das Kundenportal
module.api:{slug}Token-Authentifizierung für Modul-APIs

Admin-Routen

Die meisten Modul-Routen richten sich an das Admin-Backend. Ein vollständiges Ressourcen-Set sieht typischerweise so aus:

Route::middleware(['web', 'auth'])
    ->prefix('admin/documents')
    ->name('admin.documents.')
    ->group(function () {
        Route::get('/', [DocumentController::class, 'index'])->name('index');
        Route::get('/create', [DocumentController::class, 'create'])->name('create');
        Route::post('/', [DocumentController::class, 'store'])->name('store');
        Route::get('/{document}', [DocumentController::class, 'show'])->name('show');
        Route::get('/{document}/download', [DocumentController::class, 'download'])->name('download');
        Route::delete('/{document}', [DocumentController::class, 'destroy'])->name('destroy');
    });

Portal-Routen (für Kunden)

Erweitert Ihr Modul das Kundenportal, schützen Sie die Routen mit dem auth:customer-Guard und der EnsureCustomer-Middleware. So sehen Kunden ausschließlich die für sie freigegebenen Bereiche — und nur, wenn das Portal überhaupt aktiviert ist.

Route::middleware(['web', 'auth:customer', EnsureCustomer::class])
    ->prefix('portal/documents')
    ->name('portal.documents.')
    ->group(function () {
        Route::get('/', [PortalDocumentController::class, 'index'])->name('index');
        Route::get('/{document}/download', [PortalDocumentController::class, 'download'])->name('download');
    });

Fahrer-Routen

Routen für die Fahrer-Oberfläche kombinieren EnsureDriver mit EnsureDsgvoInformed. Der zweite Wächter stellt sicher, dass der Fahrer die Datenschutz-Hinweise bestätigt hat, bevor er eine Seite erreicht, auf der personenbezogene Daten verarbeitet werden — ein bewusst eingebauter Schritt, kein optionales Extra.

Route::middleware(['web', 'auth', EnsureDriver::class, EnsureDsgvoInformed::class])
    ->prefix('driver/my-feature')
    ->name('driver.my-feature.')
    ->group(function () {
        Route::get('/', [DriverFeatureController::class, 'index'])->name('index');
    });

Modul-API-Routen

Neben Web-Routen kann ein Modul auch maschinenlesbare Endpunkte anbieten — etwa für einen Webhook oder einen Statusabruf durch ein externes System. Dafür gibt es den ModuleApiRegistrar. Er erzeugt authentifizierte API-Endpunkte mit einer einheitlichen URL- und Namensstruktur, sodass Sie sich nicht selbst um Versionierung und Token-Prüfung kümmern müssen.

Hinweis zur Abgrenzung: Gemeint ist hier die modul-eigene API-Mechanik, die heute vorhanden und nutzbar ist. Eine übergreifende, öffentliche REST-API der Anwendung ist davon getrennt und weiterhin geplant — sie ist nicht Gegenstand dieser Seite.

Registrierung

protected function registerApiRoutes(): void
{
    $registrar = app(ModuleApiRegistrar::class);

    $registrar->routes('my-module', 1, function () {
        Route::get('status', [MyApiController::class, 'status'])->name('status');
        Route::post('webhook', [MyApiController::class, 'webhook'])->name('webhook');
    });
}

Der zweite Parameter (1) ist die API-Version. Sie ist Teil der erzeugten URL — dadurch können Sie eine v2 später hinzufügen, ohne bestehende Integrationen zu brechen, die noch auf v1 zeigen.

Erzeugte URL-Struktur

Aus der Registrierung baut der Registrar Pfade nach diesem Schema:

/api/mod/{slug}/v{version}/{endpoint}

Aus dem Beispiel oben wird etwa /api/mod/my-module/v1/status. Der Slug und die Version stecken fest in der URL — das macht von außen sofort erkennbar, welches Modul in welcher Version antwortet.

Routennamen

Die Namen der API-Routen folgen dem Muster module.{slug}.api.v{version}.{name}, also zum Beispiel module.my-module.api.v1.status. Auch hier sorgt das feste Schema dafür, dass sich API-Routen verschiedener Module nie in die Quere kommen.

API-Authentifizierung

Modul-API-Routen sind über die Middleware module.api:{slug} (AuthenticateModuleApi) geschützt. Sie sind also nicht offen erreichbar, sondern verlangen ein gültiges Token. Der Ablauf bei jeder Anfrage:

  1. erwartet ein Bearer-Token im Authorization-Header
  2. prüft den Token-Hash gegen die Tabelle module_api_tokens
  3. kontrolliert die Berechtigungen (abilities) und das Ablaufdatum des Tokens
  4. antwortet bei Ungültigkeit mit 401 als JSON
Authorization: Bearer <token>

Das Token-Modell

Gespeichert wird nie das Klartext-Token, sondern nur dessen Hash. Das ist der Grund, warum ein Token nach dem Erzeugen einmalig angezeigt und danach nicht mehr rekonstruiert werden kann — aus dem Hash lässt sich das Original nicht zurückrechnen.

// ModuleApiToken fields:
module_slug     // which module this token belongs to
name            // human-readable token name
token_hash      // SHA-256 hash of the actual token
abilities       // JSON array of allowed abilities
expires_at      // optional expiration datetime
last_used_at    // updated on each use

Verwaltet werden die Token über die Admin-Oberfläche (AdminModuleApiTokenController) — Sie müssen sie also nicht von Hand in der Datenbank anlegen.

Beispiel-Controller

Ein API-Controller gibt typischerweise JSON zurück. Achten Sie darauf, eingehende Daten zu validieren, bevor Sie sie verarbeiten — validate() lehnt unpassende Anfragen mit einer klaren Fehlerantwort ab, statt sie ungeprüft durchzulassen.

namespace Schneespur\Module\MyModule\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class MyApiController
{
    public function status(): JsonResponse
    {
        return response()->json([
            'status' => 'ok',
            'module' => 'my-module',
            'version' => '1.0.0',
        ]);
    }

    public function webhook(Request $request): JsonResponse
    {
        $payload = $request->validate([
            'event' => 'required|string',
            'data' => 'required|array',
        ]);

        // Process webhook...

        return response()->json(['received' => true]);
    }
}

Vorhandene Core-Middleware im Überblick

Bevor Sie eine eigene Middleware schreiben, lohnt der Blick auf die bereits vorhandenen Wächter. Viele Zugriffsregeln sind damit schon abgedeckt:

MiddlewareKlasseZweck
EnsureAdminApp\Http\Middleware\EnsureAdminAdmin-Rolle verlangen
EnsureDriverApp\Http\Middleware\EnsureDriverFahrer-Rolle verlangen
EnsureCustomerApp\Http\Middleware\EnsureCustomerKunden-Guard + aktiviertes Portal verlangen
EnsureDsgvoInformedApp\Http\Middleware\EnsureDsgvoInformedbestätigten Datenschutz-Hinweis verlangen
AuthenticateOwntracksApp\Http\Middleware\AuthenticateOwntracksHTTP-Basic für OwnTracks-GPS
AuthenticateModuleApiApp\Http\Middleware\AuthenticateModuleApiBearer-Token für Modul-APIs
InstallerGuardApp\Http\Middleware\InstallerGuardInstaller-Routen schützen
RedirectToInstallerApp\Http\Middleware\RedirectToInstallerumleiten, solange nicht installiert (robots.txt ist ausgenommen)
SetInstallerLocaleApp\Http\Middleware\SetInstallerLocaleSprache des Installers setzen
SecurityHeadersApp\Http\Middleware\SecurityHeadersHärtungs-Header plus pro-Anfrage gesetztes X-Robots-Tag (siehe unten)

Die SecurityHeaders-Middleware hängt an der web-Gruppe und läuft damit für jede Web-Antwort — auch für Modul-Routen. Hinzugekommen in 1.1.6. Neben den üblichen Härtungs-Headern (Clickjacking, MIME, Referrer, HSTS) setzt sie auf jeder Antwort X-Robots-Tag: noindex, nofollowaußer für die öffentlichen Pfade, die ein Frontpage-Modul über die PublicHomepageRegistry freigibt. Reichen Sie öffentliche Modul-Routen deshalb durch die web-Gruppe, damit dieser Header korrekt greift.

Öffentliche Startseite & öffentliche Routen

Hinzugekommen in 1.1.6. Standardmäßig ist eine Installation privat: Die Wurzel-URL / leitet auf den Login um, und jede Antwort trägt einen noindex-Header. Ein Frontpage-Modul kann diese Wurzel-URL übernehmen und einzelne Seiten öffentlich und indexierbar machen, ohne den Rest der Anwendung zu öffnen. Hier geht es nur um die Routing-Mechanik dahinter — die vollständige Erklärung der Indexierung steht auf einer eigenen Seite (siehe unten).

Die Routing-Mechanik existiert bereits. Das Frontpage-Modul, das sie nutzt, ist hingegen noch nicht verfügbar und weiterhin geplant.

Wie die /-Route entscheidet

Die Core-Route für / liegt in routes/web.php. Sie fragt bei jeder Anfrage die PublicHomepageRegistry ab, statt einmal beim Booten festgelegt zu werden. Das ist der Grund, warum die Übernahme der Startseite auch mit route:cache funktioniert — die Entscheidung fällt zur Request-Zeit, nicht beim Cachen der Routen.

Welche Antwort / liefert, folgt einer festen Vorrangordnung:

  1. Installer — solange die Installation nicht abgeschlossen ist, gewinnt der Installer.
  2. registrierte Startseite — danach gewinnt eine über die Registry registrierte Homepage.
  3. Redirect auf Login — ist keine Startseite registriert, bleibt das bisherige Standardverhalten: / leitet auf den Login um.

Öffentliche Zusatzrouten

Weitere öffentliche Seiten registrieren Sie als ganz normale Web-Routen (siehe oben). Wichtig ist nur: Sie müssen durch die web-Middleware-Gruppe laufen. Nur dann greift die SecurityHeaders-Middleware, die den X-Robots-Tag-Header pro Anfrage setzt — und für freigegebene Pfade eben weglässt.

// Im ServiceProvider::boot() eines (geplanten) Frontpage-Moduls
Route::middleware('web')->group(function () {
    Route::get('/leistungen', [SiteController::class, 'services'])->name('frontpage.services');
});

Welche dieser Routen tatsächlich öffentlich sein sollen, melden Sie anschließend der PublicHomepageRegistry an. Alles, was Sie nicht freigeben, bleibt privat (Default-Deny).

Die Registry-API (register(), allowCrawling() usw.) ist in der Registries-Referenz beschrieben. Das große Bild — die dynamische robots.txt-Route, der X-Robots-Tag-Header und das <meta name="robots">-Opt-in als drei Indexierungs-Ebenen — steht unter Öffentliche Startseite & SEO.

Eigene Middleware schreiben

Reicht keiner der vorhandenen Wächter, registrieren Sie eine eigene Middleware mit einem Alias in Ihrem ServiceProvider. Über diesen Alias sprechen Ihre Routen die Middleware dann genauso an wie die Core-Wächter:

public function boot(): void
{
    $router = app('router');
    $router->aliasMiddleware('my-module.check', MyModuleCheckMiddleware::class);
}

Mehr zum ServiceProvider als zentralem Einstiegspunkt steht unter ServiceProvider; welche Rollen und Berechtigungen die Wächter prüfen, behandelt Berechtigungen & Rollen.