mirror of
git://f0xx.org/ac/ac-deploy
synced 2026-07-29 02:57:38 +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:
@@ -58,6 +58,9 @@ GITEA_ORG=ac
|
|||||||
LAB_PUBLIC_ORIGIN=https://acl0.f0xx.org
|
LAB_PUBLIC_ORIGIN=https://acl0.f0xx.org
|
||||||
PUBLIC_ORIGIN=https://apps.f0xx.org
|
PUBLIC_ORIGIN=https://apps.f0xx.org
|
||||||
SHORT_LINKS_PUBLIC_BASE=https://s.f0xx.org
|
SHORT_LINKS_PUBLIC_BASE=https://s.f0xx.org
|
||||||
|
# Lab cluster → prod url-shortener (cast01 has no outbound :443 to s.f0xx.org; use BE vhost + Host)
|
||||||
|
URL_SHORTENER_API_BASE=http://artc0.intra.raptor.org
|
||||||
|
URL_SHORTENER_API_HOST=s.f0xx.org
|
||||||
FE_PROXY_TARGET=cast01.intra.raptor.org:80
|
FE_PROXY_TARGET=cast01.intra.raptor.org:80
|
||||||
|
|
||||||
# Lab DNS (2026-06): acl0.f0xx.org + c1–c3.acl0.f0xx.org → FE (cast01–03 cluster)
|
# Lab DNS (2026-06): acl0.f0xx.org + c1–c3.acl0.f0xx.org → FE (cast01–03 cluster)
|
||||||
|
|||||||
@@ -492,7 +492,18 @@ final class ShortLinksRepository {
|
|||||||
public static function shortenForOutboundMail(string $longUrl, int $ttlSeconds = 86400): array {
|
public static function shortenForOutboundMail(string $longUrl, int $ttlSeconds = 86400): array {
|
||||||
$longUrl = trim($longUrl);
|
$longUrl = trim($longUrl);
|
||||||
$fallback = ['short_url' => $longUrl, 'qr_scan_url' => $longUrl, 'qr_image_url' => null];
|
$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;
|
return $fallback;
|
||||||
}
|
}
|
||||||
$bearerId = self::resolveOutboundBearerId();
|
$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. */
|
/** Short URL with src=qr for QR encoding and phone scan analytics. */
|
||||||
public static function qrScanUrl(string $shortUrl): string {
|
public static function qrScanUrl(string $shortUrl): string {
|
||||||
return self::appendQueryParam($shortUrl, 'src', 'qr');
|
return self::appendQueryParam($shortUrl, 'src', 'qr');
|
||||||
|
|||||||
@@ -138,6 +138,13 @@ unset _MSMTP_PASS _mail_from_secret
|
|||||||
MAIL_FROM_PHP="$(printf '%s' "$MAIL_FROM" | sed "s/'/\\\\'/g")"
|
MAIL_FROM_PHP="$(printf '%s' "$MAIL_FROM" | sed "s/'/\\\\'/g")"
|
||||||
AUTH_BEARER_ID="$(read_secret URL_SHORTENER_AUTH_BEARER_ID 2>/dev/null || true)"
|
AUTH_BEARER_ID="$(read_secret URL_SHORTENER_AUTH_BEARER_ID 2>/dev/null || true)"
|
||||||
[ -n "$AUTH_BEARER_ID" ] || AUTH_BEARER_ID="${URL_SHORTENER_AUTH_BEARER_ID:-0}"
|
[ -n "$AUTH_BEARER_ID" ] || AUTH_BEARER_ID="${URL_SHORTENER_AUTH_BEARER_ID:-0}"
|
||||||
|
API_BEARER_TOKEN="$(read_secret URL_SHORTENER_API_BEARER_TOKEN 2>/dev/null || true)"
|
||||||
|
API_BEARER_PHP=""
|
||||||
|
if [ -n "$API_BEARER_TOKEN" ]; then
|
||||||
|
API_BEARER_PHP="$(printf '%s' "$API_BEARER_TOKEN" | sed "s/'/\\\\'/g")"
|
||||||
|
fi
|
||||||
|
URL_SHORTENER_API_BASE_CFG="${URL_SHORTENER_API_BASE:-${SHORT_LINKS_PUBLIC_BASE}}"
|
||||||
|
URL_SHORTENER_API_HOST_CFG="${URL_SHORTENER_API_HOST:-}"
|
||||||
mkdir -p "${COMPOSED}/config"
|
mkdir -p "${COMPOSED}/config"
|
||||||
log "write composed config.php base_path=${ISSUES_BASE}"
|
log "write composed config.php base_path=${ISSUES_BASE}"
|
||||||
cat > "${COMPOSED}/config/config.php" <<PHP
|
cat > "${COMPOSED}/config/config.php" <<PHP
|
||||||
@@ -190,6 +197,9 @@ return [
|
|||||||
'url_shortener' => [
|
'url_shortener' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'public_base' => '${SHORT_LINKS_PUBLIC_BASE}',
|
'public_base' => '${SHORT_LINKS_PUBLIC_BASE}',
|
||||||
|
'api_base' => '${URL_SHORTENER_API_BASE_CFG}',
|
||||||
|
'api_host' => '${URL_SHORTENER_API_HOST_CFG}',
|
||||||
|
'api_bearer_token' => '${API_BEARER_PHP}',
|
||||||
'auth_bearer_id' => ${AUTH_BEARER_ID:-0},
|
'auth_bearer_id' => ${AUTH_BEARER_ID:-0},
|
||||||
'db' => [
|
'db' => [
|
||||||
'mysql' => [
|
'mysql' => [
|
||||||
|
|||||||
31
sim/cluster0/scripts/ensure-auth-email-api-bearer.sh
Executable file
31
sim/cluster0/scripts/ensure-auth-email-api-bearer.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Store prod s.f0xx.org API bearer for lab outbound mail (links must live in prod DB).
|
||||||
|
set -eu
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$ROOT/scripts/lib/common.sh"
|
||||||
|
load_cluster_env
|
||||||
|
|
||||||
|
TOKEN="${1:-}"
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
die "usage: ensure-auth-email-api-bearer.sh <bearer_token>
|
||||||
|
Mint on prod BE: php81 /var/www/localhost/htdocs/apps/s/scripts/seed_bearer.php --label=auth-email-system"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SECRETS="${CAST_CLUSTER_ROOT}/secrets.lab.env"
|
||||||
|
touch "$SECRETS"
|
||||||
|
chmod 600 "$SECRETS"
|
||||||
|
grep -v '^URL_SHORTENER_API_BEARER_TOKEN=' "$SECRETS" > /tmp/secrets.new || true
|
||||||
|
printf 'URL_SHORTENER_API_BEARER_TOKEN=%s\n' "$TOKEN" >> /tmp/secrets.new
|
||||||
|
mv /tmp/secrets.new "$SECRETS"
|
||||||
|
chmod 600 "$SECRETS"
|
||||||
|
|
||||||
|
_short="$(curl -fsS -X POST "${URL_SHORTENER_API_BASE:-http://artc0.intra.raptor.org}/api/v1/shorten" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "Host: ${URL_SHORTENER_API_HOST:-s.f0xx.org}" \
|
||||||
|
-d "{\"url\":\"https://example.com/lab-smoke\",\"bearer\":\"${TOKEN}\",\"ttl\":60}" \
|
||||||
|
| php83 -r '$j=json_decode(stream_get_contents(STDIN),true); echo (string)($j["u"]??"");' 2>/dev/null)" || _short=""
|
||||||
|
[ -n "$_short" ] || die "API smoke failed — token rejected by ${SHORT_LINKS_PUBLIC_BASE:-https://s.f0xx.org}"
|
||||||
|
|
||||||
|
log "ensure-auth-email-api-bearer_ok smoke=${_short}"
|
||||||
|
log "next: compose-lab-backend.sh && verify-mail-lab.sh …"
|
||||||
@@ -26,3 +26,7 @@ MAIL_REPLY_TO=bestcastr@gmail.com
|
|||||||
MSMTP_FROM=bestcastr@gmail.com
|
MSMTP_FROM=bestcastr@gmail.com
|
||||||
MSMTP_USER=bestcastr@gmail.com
|
MSMTP_USER=bestcastr@gmail.com
|
||||||
# MSMTP_APP_PASSWORD="xxxx xxxx xxxx xxxx"
|
# MSMTP_APP_PASSWORD="xxxx xxxx xxxx xxxx"
|
||||||
|
|
||||||
|
# Prod s.f0xx.org shorten API — required for verify-email short links from lab (lab MariaDB ≠ prod).
|
||||||
|
# Mint on alpine-be: php81 …/apps/s/scripts/seed_bearer.php --label=auth-email-system
|
||||||
|
# URL_SHORTENER_API_BEARER_TOKEN=
|
||||||
|
|||||||
Reference in New Issue
Block a user