presseportale/app/Services/Api/LegacyApiKeyImporter.php
Kevin Adametz 616b7847d4 Uebernahme bestehender Legacy-API-Integrationen (Keys, Auth, Pfade)
Bestehende Kunden-Integrationen der Altportale laufen nach dem Relaunch
weiter, ohne dass Kunden etwas umstellen muessen:

1. legacy:import-api-keys uebernimmt die api_keys aus den Legacy-DBs
   (sf_guard_user_profile) als Sanctum-Tokens mit identischem Secret
   (Sanctum loest Tokens ohne id|-Praefix per sha256-Hash auf) — nur
   fuer berechtigte Nutzer (aktiver Zahlungsstatus/Bestandsschutz,
   ApiAccessEligibilityService); idempotent, mit Dry-Run. Probe gegen
   die lokalen Snapshots: 406 Keys wuerden uebernommen.
2. Die Auth-Middleware uebersetzt die alten Uebergabeformen (X-ApiKey-
   Header, api_key-Parameter) fuer gueltige Keys in Bearer; unbekannte
   Keys erhalten weiterhin 410 mit Migrationshinweis.
3. Die dokumentierten /service/...-Pfade der Alt-API sind als Aliasse
   auf die v1-Controller gelegt (pressreleases inkl. Bilder, companies
   lesend); nicht uebernommene Alt-Endpoints (Pressemappen/Kontakte
   anlegen) antworten 410 mit Hinweis auf Kundenkonto und neue Doku.

API-Seite + OpenAPI entsprechend aktualisiert; Go-Live-Checkliste um den
Import-Schritt (nach dem Grandfathering) ergaenzt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:55:22 +00:00

90 lines
2.6 KiB
PHP

<?php
namespace App\Services\Api;
use App\Models\User;
use Laravel\Sanctum\PersonalAccessToken;
/**
* Übernahme bestehender Legacy-API-Integrationen (User-Entscheidung
* 06.07.2026): Die api_keys der Altportale (sf_guard_user_profile) werden
* als Sanctum-Tokens mit identischem Secret importiert — Sanctum löst
* Tokens ohne "id|"-Präfix über den sha256-Hash auf, dadurch bleiben die
* bestehenden Schlüssel der Kunden unverändert gültig. Berechtigt ist,
* wer auch selbst ein Token erstellen dürfte (ApiAccessEligibilityService).
*/
class LegacyApiKeyImporter
{
public const TOKEN_NAME = 'Legacy-API-Key (importiert)';
public const ABILITIES = [
'press-releases:read',
'press-releases:write',
'press-release-images:write',
'companies:read',
'newsletter:subscribe',
];
public function __construct(private readonly ApiAccessEligibilityService $eligibility) {}
/**
* @param iterable<int, array{legacy_id: int, api_key: string}> $rows
* @return array{imported: int, already_present: int, ineligible: int, unknown_user: int, ineligible_users: list<string>}
*/
public function import(string $portal, iterable $rows, bool $dryRun = false): array
{
$result = [
'imported' => 0,
'already_present' => 0,
'ineligible' => 0,
'unknown_user' => 0,
'ineligible_users' => [],
];
foreach ($rows as $row) {
$key = trim((string) $row['api_key']);
if ($key === '') {
continue;
}
$user = User::query()
->where('legacy_portal', $portal)
->where('legacy_id', (int) $row['legacy_id'])
->first();
if (! $user) {
$result['unknown_user']++;
continue;
}
$hash = hash('sha256', $key);
if (PersonalAccessToken::query()->where('token', $hash)->exists()) {
$result['already_present']++;
continue;
}
if (! $this->eligibility->canCreateToken($user)) {
$result['ineligible']++;
$result['ineligible_users'][] = (string) $user->email;
continue;
}
if (! $dryRun) {
$user->tokens()->create([
'name' => self::TOKEN_NAME,
'token' => $hash,
'abilities' => self::ABILITIES,
]);
}
$result['imported']++;
}
return $result;
}
}