Files
landing-api/app/Support/PreviewLink.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

75 lines
2.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
/**
* A shareable link to a draft on the public site.
*
* The shared `SITE_PREVIEW_TOKEN` still exists, but it is one secret, never
* rotated, that has to be pasted into a curl command. This is the same idea
* made usable: a link somebody can send to whoever needs to look, scoped to
* one record and one language, and useless a few hours later.
*
* The token carries the record it points at rather than being a bearer of
* "show me anything", so passing it around leaks one draft rather than every
* unpublished thing on the site.
*/
final class PreviewLink
{
/**
* How long a link stays good. Long enough to send it and get an answer,
* short enough that one forwarded in an e-mail thread stops working.
*/
public const HOURS = 8;
public static function for(Model $record, string $type, string $locale): ?string
{
$slug = $record->getTranslation('slug', $locale, useFallbackLocale: false);
// No slug in this language means no address to preview.
if (blank($slug)) {
return null;
}
$path = SiteUrls::path($type, $locale, (string) $slug);
$token = Crypt::encryptString(json_encode([
'type' => $type,
'id' => $record->getKey(),
'locale' => $locale,
'expires' => now()->addHours(self::HOURS)->timestamp,
], JSON_THROW_ON_ERROR));
return rtrim((string) config('site.url'), '/').$path.'?preview='.urlencode($token);
}
/**
* What a token says, or null when it is expired, forged or unreadable.
*
* @return array{type: string, id: int|string, locale: string}|null
*/
public static function open(string $token): ?array
{
try {
$payload = json_decode(Crypt::decryptString($token), true, flags: JSON_THROW_ON_ERROR);
} catch (\Throwable) {
return null;
}
if (! is_array($payload) || ($payload['expires'] ?? 0) < now()->timestamp) {
return null;
}
return [
'type' => (string) $payload['type'],
'id' => $payload['id'],
'locale' => (string) $payload['locale'],
];
}
}