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:
2026-07-05 14:21:55 +07:00
parent 4f1efc6ff9
commit 818d4fa384
5 changed files with 59 additions and 4 deletions
+18
View File
@@ -1,5 +1,23 @@
export const API_BASE = import.meta.env.VITE_API_BASE || "/api/v1";
let configPromise: Promise<{ bot_username: string }> | null = null;
/// Юзернейм бота — намеренно не `import.meta.env.VITE_BOT_USERNAME`: этот SPA
/// собирается один раз в CI, и один и тот же статический бандл раздаётся и
/// прод-, и dev-стендом — build-time значение зашилось бы в файлы намертво и
/// не различалось бы между стендами (ровно так и вышло на практике: dev видел
/// прод-бота, у которого в BotFather другой домен — виджет логина отвечал
/// "Bot domain invalid"). `/api/v1/config` читает env на сервере заново на
/// каждый запрос, поэтому корректно отдаёт "свой" бот для каждого стенда.
export function getPublicConfig(): Promise<{ bot_username: string }> {
if (!configPromise) {
configPromise = fetch(`${API_BASE}/config`)
.then((res) => res.json())
.catch(() => ({ bot_username: "ntrnr_vpn_bot" }));
}
return configPromise;
}
// Сессия живёт только в HttpOnly cookie, которую ставит сервер — JS её не видит
// и не должен: раньше здесь читался `document.cookie` в расчёте на токен,
// который сервер ставит как HttpOnly, поэтому чтение всегда возвращало null, а