Serve bot_username at runtime instead of baking it into the SPA build
Auth.tsx and Profile.tsx read import.meta.env.VITE_BOT_USERNAME — a Vite build-time constant. This ЛК frontend is a single static bundle built once in CI and served identically by both prod and dev (no server-rendering to re-inject a value per environment, unlike the Next.js landing fix from earlier). So the Telegram Login Widget on the dev stand was rendering with whatever bot the CI build happened to be compiled with — in practice the fallback default, the prod bot's username — and Telegram correctly refused it with "Bot domain invalid" since that bot's registered domain is the prod domain, not dev.netrunner-vpn.com. Added GET /api/v1/config (unauthenticated, reads BOT_USERNAME from the server's actual environment on every request) and switched both files to fetch it at runtime instead of reading the baked-in env var. Verified locally: the dev container's /api/v1/config correctly returns its own dev bot's username, and a real headless Chrome run confirms the Login Widget script tag gets that value, not the hardcoded fallback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+17
@@ -43,6 +43,15 @@ pub struct ApiState {
|
||||
pub jwt_secret: String,
|
||||
pub bot_token: String,
|
||||
pub admin_tg_id: i64,
|
||||
/// Юзернейм бота (без `@`) — отдаётся фронту через `GET /api/v1/config`.
|
||||
/// Не публичный build-time env (`VITE_BOT_USERNAME`): фронт — статический
|
||||
/// SPA-бандл, собираемый один раз в CI и одинаковый что для прод-, что для
|
||||
/// dev-деплоя, значение зашилось бы намертво в конкретную сборку и не
|
||||
/// подхватывало бы разные боты на разных стендах (ровно так и оказалось на
|
||||
/// практике — dev-стенд показывал прод-бота, у которого домен в BotFather
|
||||
/// не совпадает с dev-доменом → "Bot domain invalid"). Читается на сервере
|
||||
/// заново на каждый запрос, поэтому корректно различается между стендами.
|
||||
pub bot_username: String,
|
||||
/// `Domain` для сессионных cookie. Пусто (по умолчанию, локальная разработка) →
|
||||
/// cookie host-only. В проде — `.netrunner-vpn.com`, чтобы одна сессия работала
|
||||
/// и на лендинге, и на ЛК (общий родительский домен, разные сабдомены).
|
||||
@@ -72,6 +81,7 @@ impl Clone for ApiState {
|
||||
jwt_secret: self.jwt_secret.clone(),
|
||||
bot_token: self.bot_token.clone(),
|
||||
admin_tg_id: self.admin_tg_id,
|
||||
bot_username: self.bot_username.clone(),
|
||||
cookie_domain: self.cookie_domain.clone(),
|
||||
cookie_secure: self.cookie_secure,
|
||||
csrf_allowed_origins: self.csrf_allowed_origins.clone(),
|
||||
@@ -119,6 +129,7 @@ pub fn create_router(state: ApiState, frontend_dir: &str) -> Router {
|
||||
.nest("/billing", bill_ctrl::router(state.clone()))
|
||||
.nest("/nodes", node_ctrl::public_router(state.clone()))
|
||||
.nest("/referrals", ref_ctrl::router(state.clone()))
|
||||
.route("/config", get(get_public_config))
|
||||
.layer(GovernorLayer::new(gov_conf));
|
||||
|
||||
let admin_routes_v1 = Router::new().nest("/nodes", node_ctrl::admin_router(state.clone()));
|
||||
@@ -163,6 +174,12 @@ pub fn create_router(state: ApiState, frontend_dir: &str) -> Router {
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// Публичная (без auth_guard) рантайм-конфигурация для статического SPA —
|
||||
/// см. `ApiState::bot_username` про то, почему это не build-time env фронта.
|
||||
async fn get_public_config(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
Json(json!({ "bot_username": state.bot_username }))
|
||||
}
|
||||
|
||||
/// Liveness: процесс жив и отвечает. Не трогает зависимости.
|
||||
async fn health_live() -> impl IntoResponse {
|
||||
(StatusCode::OK, Json(json!({ "status": "ok" })))
|
||||
|
||||
Reference in New Issue
Block a user