media: PM-Medien privat ausliefern + Vorschau-Signatur (M4, M5, Fix#4/#5)
- Neue PM-Bilder/Anhänge liegen auf der privaten local-Disk; Auslieferung über kontrollierte Media-Routen mit gemeinsamer Zugriffslogik (AuthorizesPressReleaseMediaAccess). Öffentliche Medien werden aus den geladenen Modellen ermittelt (weniger Queries) und länger gecacht. - Anonyme Token-Vorschau lädt Bilder über temporär signierte URLs (Fix#5). - API-Resource baut Variant-URLs über die Media-Route statt /storage (Fix#4). Storage-Schicht prüft zusätzlich den serverseitigen MIME-Type (M5). - press-releases:privatize-media migriert Bestandsmedien. Deployment: einmalig `php artisan press-releases:privatize-media` ausführen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0b35c3b04d
commit
000ae93e3b
21 changed files with 588 additions and 70 deletions
|
|
@ -3,9 +3,11 @@
|
|||
use App\Enums\Portal;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Volt\Volt as LivewireVolt;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
|
@ -151,6 +153,67 @@ test('preview link allows access to draft press release without authentication',
|
|||
expect(auth()->check())->toBeFalse();
|
||||
});
|
||||
|
||||
test('preview media of an unpublished release is reachable via the signed url but not unsigned', function () {
|
||||
/** @var TestCase $this */
|
||||
Storage::fake('local');
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$pr = PressRelease::factory()->forPortal(Portal::Presseecho)->create([
|
||||
'user_id' => $owner->id,
|
||||
'status' => PressReleaseStatus::Draft->value,
|
||||
'title' => 'Entwurf mit Bild',
|
||||
]);
|
||||
|
||||
$path = "press-releases/{$pr->id}/images/entwurf-detail.jpg";
|
||||
Storage::disk('local')->put($path, 'binary');
|
||||
|
||||
$image = PressReleaseImage::query()->create([
|
||||
'press_release_id' => $pr->id,
|
||||
'disk' => 'local',
|
||||
'path' => $path,
|
||||
'variants' => ['detail' => $path],
|
||||
'is_preview' => true,
|
||||
]);
|
||||
|
||||
// Ohne Signatur ist das Medium einer unveröffentlichten PM anonym gesperrt.
|
||||
$this->get(route('media.press-release-images.show', ['image' => $image->id, 'variant' => 'detail']))
|
||||
->assertNotFound();
|
||||
|
||||
// Die im Vorschaumodus erzeugte, signierte URL liefert das Bild aus.
|
||||
$signedUrl = $image->variantUrl('detail', signed: true);
|
||||
expect($signedUrl)->toContain('signature=');
|
||||
|
||||
$this->get($signedUrl)->assertOk();
|
||||
});
|
||||
|
||||
test('preview page emits signed media urls for the hero image', function () {
|
||||
/** @var TestCase $this */
|
||||
Storage::fake('local');
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$pr = PressRelease::factory()->forPortal(Portal::Presseecho)->create([
|
||||
'user_id' => $owner->id,
|
||||
'status' => PressReleaseStatus::Draft->value,
|
||||
]);
|
||||
|
||||
$path = "press-releases/{$pr->id}/images/hero-detail.jpg";
|
||||
Storage::disk('local')->put($path, 'binary');
|
||||
PressReleaseImage::query()->create([
|
||||
'press_release_id' => $pr->id,
|
||||
'disk' => 'local',
|
||||
'path' => $path,
|
||||
'variants' => ['detail' => $path],
|
||||
'is_preview' => true,
|
||||
]);
|
||||
|
||||
$share = app(MagicLinkGenerator::class)->createPressReleaseShareLink($pr, $owner);
|
||||
|
||||
$this->get(route('press-releases.preview', ['token' => $share['plain_token']]))
|
||||
->assertOk()
|
||||
->assertSee('media/press-release-images', false)
|
||||
->assertSee('signature=', false);
|
||||
});
|
||||
|
||||
test('expired preview link returns 410 gone', function () {
|
||||
/** @var TestCase $this */
|
||||
$owner = User::factory()->create();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue