mirror of
git://f0xx.org/ac/ac-deploy
synced 2026-07-29 07:37:47 +03:00
cluster0: lab verify-email short links via prod s.f0xx.org API
Lab MariaDB slugs are not on public s.f0xx.org; outbound mail calls prod shorten API (internal artc0 + Host header) with API bearer token in secrets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -492,7 +492,18 @@ final class ShortLinksRepository {
|
||||
public static function shortenForOutboundMail(string $longUrl, int $ttlSeconds = 86400): array {
|
||||
$longUrl = trim($longUrl);
|
||||
$fallback = ['short_url' => $longUrl, 'qr_scan_url' => $longUrl, 'qr_image_url' => null];
|
||||
if ($longUrl === '' || !UrlShortenerDatabase::enabled() || !UrlShortenerDatabase::ping()) {
|
||||
if ($longUrl === '') {
|
||||
return $fallback;
|
||||
}
|
||||
$apiToken = trim((string) cfg('url_shortener.api_bearer_token', ''));
|
||||
if ($apiToken !== '') {
|
||||
$viaApi = self::shortenViaPublicApi($longUrl, $ttlSeconds, $apiToken);
|
||||
if ($viaApi !== null) {
|
||||
return $viaApi;
|
||||
}
|
||||
error_log('ShortLinksRepository: outbound API shorten failed, trying local DB');
|
||||
}
|
||||
if (!UrlShortenerDatabase::enabled() || !UrlShortenerDatabase::ping()) {
|
||||
return $fallback;
|
||||
}
|
||||
$bearerId = self::resolveOutboundBearerId();
|
||||
@@ -514,6 +525,66 @@ final class ShortLinksRepository {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create short link on public s.f0xx.org (prod DB) — required when lab MariaDB is not served by s.f0xx.org.
|
||||
*
|
||||
* @return array{short_url:string,qr_scan_url:string,qr_image_url:?string}|null
|
||||
*/
|
||||
public static function shortenViaPublicApi(string $longUrl, int $ttlSeconds, string $bearerToken): ?array {
|
||||
$apiBase = rtrim(
|
||||
(string) cfg('url_shortener.api_base', cfg('url_shortener.public_base', 'https://s.f0xx.org')),
|
||||
'/'
|
||||
);
|
||||
$apiHost = trim((string) cfg('url_shortener.api_host', ''));
|
||||
$payload = json_encode([
|
||||
'url' => $longUrl,
|
||||
'bearer' => $bearerToken,
|
||||
'ttl' => max(0, $ttlSeconds),
|
||||
], JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($payload)) {
|
||||
return null;
|
||||
}
|
||||
$headers = "Content-Type: application/json\r\nAccept: application/json\r\n";
|
||||
if ($apiHost !== '') {
|
||||
$headers .= 'Host: ' . $apiHost . "\r\n";
|
||||
}
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => $headers,
|
||||
'content' => $payload,
|
||||
'timeout' => 15,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
$raw = @file_get_contents($apiBase . '/api/v1/shorten', false, $ctx);
|
||||
if (!is_string($raw) || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data) || ($data['code'] ?? '') !== '0') {
|
||||
error_log('ShortLinksRepository: API shorten error: ' . substr($raw, 0, 240));
|
||||
return null;
|
||||
}
|
||||
$shortUrl = (string) ($data['u'] ?? '');
|
||||
if ($shortUrl === '') {
|
||||
return null;
|
||||
}
|
||||
$qrImage = (string) ($data['qr'] ?? '');
|
||||
if ($qrImage === '' && preg_match('#/([a-f0-9]{6,16})$#', $shortUrl, $m)) {
|
||||
$qrImage = $apiBase . '/api/v1/qr/' . $m[1] . '.png';
|
||||
}
|
||||
return [
|
||||
'short_url' => $shortUrl,
|
||||
'qr_scan_url' => self::qrScanUrl($shortUrl),
|
||||
'qr_image_url' => $qrImage !== '' ? $qrImage : null,
|
||||
];
|
||||
}
|
||||
|
||||
/** Short URL with src=qr for QR encoding and phone scan analytics. */
|
||||
public static function qrScanUrl(string $shortUrl): string {
|
||||
return self::appendQueryParam($shortUrl, 'src', 'qr');
|
||||
|
||||
Reference in New Issue
Block a user