1
0
mirror of git://f0xx.org/ac/ac-deploy synced 2026-07-29 07:37:47 +03:00

fix(cluster0): Gitea cluster proxy, 2FA seed, hub downloads nginx

Bind Gitea on all interfaces for cast02/03 proxy, fix stale 2FA lab-seed
approve flow, add /v0/downloads/ and GITEA_UPSTREAM_HOST, and guard nginx
configure against empty templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-07-10 15:36:49 +02:00
parent 970de79a90
commit a14413e886
6 changed files with 78 additions and 16 deletions

View File

@@ -51,6 +51,8 @@ POSTGRES_SEMI_ENABLE=1
# Gitea browse UI on cast01 (primary); mirrors added later # Gitea browse UI on cast01 (primary); mirrors added later
GITEA_ENABLE=1 GITEA_ENABLE=1
GITEA_HTTP_PORT=3000 GITEA_HTTP_PORT=3000
# nginx on all cluster nodes proxies /git/ to primary (cast01) — see apps-port80.conf
GITEA_UPSTREAM_HOST=10.7.16.236
GITEA_DB_TYPE=sqlite3 GITEA_DB_TYPE=sqlite3
GITEA_PUBLIC_HOST=acl0.f0xx.org GITEA_PUBLIC_HOST=acl0.f0xx.org
GITEA_ROOT_URL=https://acl0.f0xx.org/app/androidcast_project/git/ GITEA_ROOT_URL=https://acl0.f0xx.org/app/androidcast_project/git/

View File

@@ -17,7 +17,8 @@ LFS_START_SERVER = true
PROTOCOL = http PROTOCOL = http
DOMAIN = @GITEA_DOMAIN@ DOMAIN = @GITEA_DOMAIN@
ROOT_URL = @GITEA_ROOT_URL@ ROOT_URL = @GITEA_ROOT_URL@
HTTP_ADDR = 127.0.0.1 # Bind on all interfaces so cast02/cast03 nginx can proxy to cast01:3000.
HTTP_ADDR = 0.0.0.0
HTTP_PORT = @GITEA_HTTP_PORT@ HTTP_PORT = @GITEA_HTTP_PORT@
APP_DATA_PATH = @GITEA_APP_DATA@ APP_DATA_PATH = @GITEA_APP_DATA@
STATIC_ROOT_PATH = /usr/share/webapps/gitea STATIC_ROOT_PATH = /usr/share/webapps/gitea

View File

@@ -1,35 +1,56 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** 2FA challenge page helpers (QR short link for mobile handoff). */ /** 2FA challenge page helpers — cross-device approval QR. */
final class AuthTwoFactorPage { final class AuthTwoFactorPage {
/** @return array{short_url?:string,qr_url?:string,long_url:string}|null */ /**
* Build the QR payload for the 2FA challenge screen.
*
* @return array{long_url:string,short_url?:string,qr_url?:string,raw_token:string}|null
*/
public static function qrPayload(): ?array { public static function qrPayload(): ?array {
$uid = Auth::pending2faUserId(); $uid = Auth::pending2faUserId();
if ($uid <= 0) { if ($uid <= 0) {
return null; return null;
} }
$project = Auth::projectBasePath(); $project = Auth::projectBasePath();
$longUrl = 'https://apps.f0xx.org' . $project . '/?ref=2fa&uid=' . $uid; $ip = Auth::clientIp();
$rawToken = AuthApproval::create($uid, session_id(), $ip);
$_SESSION['pending_2fa_approval_token'] = $rawToken;
$origin = rtrim((string) cfg('public_origin', 'https://apps.f0xx.org'), '/');
$approveUrl = $origin . $project . '/two-factor/approve?token=' . urlencode($rawToken);
$payload = [
'long_url' => $approveUrl,
'raw_token' => $rawToken,
'qr_url' => AuthTotp::qrImageUrl($approveUrl),
];
if (!UrlShortenerDatabase::enabled() || !UrlShortenerDatabase::ping()) { if (!UrlShortenerDatabase::enabled() || !UrlShortenerDatabase::ping()) {
return ['long_url' => $longUrl]; return $payload;
} }
$bearers = ShortLinksRepository::listBearers(); $bearers = ShortLinksRepository::listBearers();
if ($bearers === []) { if ($bearers === []) {
return ['long_url' => $longUrl]; return $payload;
} }
$bearerId = (int) ($bearers[0]['id'] ?? 0); $bearerId = (int) ($bearers[0]['id'] ?? 0);
if ($bearerId <= 0) { if ($bearerId <= 0) {
return ['long_url' => $longUrl]; return $payload;
} }
$mint = ShortLinksRepository::createLink($bearerId, $longUrl, 900); $mint = ShortLinksRepository::createLink($bearerId, $approveUrl, 900);
if (empty($mint['ok'])) { if (empty($mint['ok'])) {
return ['long_url' => $longUrl]; return $payload;
} }
return [ $shortUrl = (string) ($mint['short_url'] ?? '');
'long_url' => $longUrl, $qrUrl = (string) ($mint['qr_url'] ?? '');
'short_url' => (string) ($mint['short_url'] ?? ''), if ($qrUrl !== '') {
'qr_url' => (string) ($mint['qr_url'] ?? ''), $payload['qr_url'] = $qrUrl;
]; }
if ($shortUrl !== '') {
$payload['short_url'] = $shortUrl;
}
return $payload;
} }
} }

View File

@@ -38,6 +38,13 @@ server {
add_header Cache-Control "public, max-age=60"; add_header Cache-Control "public, max-age=60";
} }
# Hub download artifacts (Session Studio jar, etc.) — populated by deploy-hub-downloads.sh
location ^~ /v0/downloads/ {
alias /var/www/localhost/htdocs/apps/app/androidcast_project/downloads/;
add_header Cache-Control "public, max-age=300";
default_type application/octet-stream;
}
# Favicon — served from hub root (shared by all sub-apps via explicit <link> tags) # Favicon — served from hub root (shared by all sub-apps via explicit <link> tags)
location = /favicon.ico { location = /favicon.ico {
alias /var/www/localhost/htdocs/apps/app/androidcast_project/favicon.ico; alias /var/www/localhost/htdocs/apps/app/androidcast_project/favicon.ico;
@@ -254,7 +261,8 @@ server {
return 301 $public_scheme://$http_host/app/androidcast_project/git/; return 301 $public_scheme://$http_host/app/androidcast_project/git/;
} }
location ^~ /app/androidcast_project/git/ { location ^~ /app/androidcast_project/git/ {
proxy_pass http://127.0.0.1:3000/; # Gitea runs on cast01 only — do not proxy to 127.0.0.1 on cast02/cast03.
proxy_pass http://10.7.16.236:3000/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;

View File

@@ -10,8 +10,13 @@ ensure_shared_mounted
GIT_NGINX="/mnt/repos/ac/ac-deploy/sim/cluster0/nginx/apps-port80.conf" GIT_NGINX="/mnt/repos/ac/ac-deploy/sim/cluster0/nginx/apps-port80.conf"
CONF_DST="$ROOT/nginx/apps-port80.conf" CONF_DST="$ROOT/nginx/apps-port80.conf"
if [ -f "$GIT_NGINX" ] && [ "$(realpath "$GIT_NGINX" 2>/dev/null)" != "$(realpath "$CONF_DST" 2>/dev/null)" ]; then if [ -f "$GIT_NGINX" ] && [ "$(realpath "$GIT_NGINX" 2>/dev/null)" != "$(realpath "$CONF_DST" 2>/dev/null)" ]; then
install -D -m 644 "$GIT_NGINX" "$CONF_DST" if [ -s "$GIT_NGINX" ]; then
install -D -m 644 "$GIT_NGINX" "$CONF_DST"
else
log "WARN: skip empty GIT_NGINX at $GIT_NGINX"
fi
fi fi
[ -s "$CONF_DST" ] || die "missing or empty nginx template at $CONF_DST"
install -D -m 644 "$CONF_DST" /etc/nginx/http.d/androidcast.conf install -D -m 644 "$CONF_DST" /etc/nginx/http.d/androidcast.conf
rm -f /etc/nginx/http.d/default.conf /etc/nginx/http.d/cluster.conf 2>/dev/null || true rm -f /etc/nginx/http.d/default.conf /etc/nginx/http.d/cluster.conf 2>/dev/null || true
nginx -t nginx -t

View File

@@ -0,0 +1,25 @@
#!/bin/sh
# Publish hub download artifacts (Session Studio jar) to BE downloads mount.
# APK is served via OTA stable channel (/v0/ota/) — run ci-build-and-publish-ota.sh separately.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=/dev/null
. "$ROOT/scripts/lib/common.sh"
load_cluster_env
DOWNLOADS_ROOT="${APP_ROOT:-/var/www/localhost/htdocs/apps/app/androidcast_project}/downloads"
WS="${AC_WORKSPACE_ROOT:-/var/www/ac/workspace}"
STUDIO_JAR="${WS}/ac-session-studio/build/libs/ac-session-studio.jar"
mkdir -p "$DOWNLOADS_ROOT"
if [ -f "$STUDIO_JAR" ]; then
install -m 644 "$STUDIO_JAR" "${DOWNLOADS_ROOT}/ac-session-studio.jar"
log "installed Session Studio jar → ${DOWNLOADS_ROOT}/ac-session-studio.jar"
else
log "WARN: Session Studio jar not built at ${STUDIO_JAR}"
log " cd ac-session-studio && ./gradlew shadowJar (or installDist) on builder node"
fi
log "Hub downloads dir: ${DOWNLOADS_ROOT}"
log "URL: ${PUBLIC_ORIGIN:-https://apps.f0xx.org}/v0/downloads/ac-session-studio.jar"