Enhance API and configuration for Telegram miniapp

- Added support for CORS origins and a new environment variable for miniapp access control.
- Implemented date validation for API requests to ensure correct date formats.
- Updated FastAPI app to allow access without Telegram initData for local development.
- Enhanced error handling and logging for better debugging.
- Added tests for API functionality and Telegram initData validation.
- Updated README with new environment variable details and testing instructions.
- Modified Docker and Git ignore files to include additional directories and files.
This commit is contained in:
2026-02-17 17:21:35 +03:00
parent 7cdf1edc34
commit 5dc8c8f255
19 changed files with 447 additions and 64 deletions

View File

@@ -13,6 +13,8 @@
const accessDeniedEl = document.getElementById("accessDenied");
const headerEl = document.querySelector(".header");
const weekdaysEl = document.querySelector(".weekdays");
const prevBtn = document.getElementById("prevMonth");
const nextBtn = document.getElementById("nextMonth");
function isoDate(d) {
return d.toISOString().slice(0, 10);
@@ -33,7 +35,33 @@
}
function getInitData() {
return (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) || "";
var fromSdk = (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) || "";
if (fromSdk) return fromSdk;
var hash = window.location.hash ? window.location.hash.slice(1) : "";
if (hash.indexOf("tgWebAppData=") === 0) {
try {
return decodeURIComponent(hash.substring("tgWebAppData=".length));
} catch (e) {
return hash.substring("tgWebAppData=".length);
}
}
var q = window.location.search ? new URLSearchParams(window.location.search).get("tgWebAppData") : null;
if (q) {
try {
return decodeURIComponent(q);
} catch (e) {
return q;
}
}
return "";
}
function getInitDataDebug() {
var sdk = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData);
var hash = window.location.hash ? window.location.hash.slice(1) : "";
var hashHasData = hash.indexOf("tgWebAppData=") === 0;
var queryHasData = !!(window.location.search && new URLSearchParams(window.location.search).get("tgWebAppData"));
return "SDK: " + (sdk ? "да" : "нет") + ", hash: " + (hashHasData ? hash.length + " симв." : "нет") + ", query: " + (queryHasData ? "да" : "нет");
}
function isLocalhost() {
@@ -49,6 +77,8 @@
loadingEl.classList.add("hidden");
errorEl.hidden = true;
accessDeniedEl.hidden = false;
var debugEl = document.getElementById("accessDeniedDebug");
if (debugEl) debugEl.textContent = getInitDataDebug();
}
function hideAccessDenied() {
@@ -65,12 +95,23 @@
const initData = getInitData();
const headers = {};
if (initData) headers["X-Telegram-Init-Data"] = initData;
const res = await fetch(url, { headers: headers });
if (res.status === 403) {
throw new Error("ACCESS_DENIED");
var controller = new AbortController();
var timeoutId = setTimeout(function () { controller.abort(); }, 15000);
try {
var res = await fetch(url, { headers: headers, signal: controller.signal });
clearTimeout(timeoutId);
if (res.status === 403) {
throw new Error("ACCESS_DENIED");
}
if (!res.ok) throw new Error("Ошибка загрузки");
return res.json();
} catch (e) {
clearTimeout(timeoutId);
if (e.name === "AbortError") {
throw new Error("Не удалось загрузить данные. Проверьте интернет.");
}
throw e;
}
if (!res.ok) throw new Error("Ошибка загрузки");
return res.json();
}
function renderCalendar(year, month, dutiesByDate) {
@@ -92,7 +133,7 @@
cell.className = "day" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (dayDuties.length ? " has-duty" : "");
cell.innerHTML =
"<span class=\"num\">" + d.getDate() + "</span>" +
(dayDuties.length ? "<span class=\"day-duties\">" + dayDuties.map(function (x) { return x.full_name; }).join(", ") + "</span>" : "");
(dayDuties.length ? "<span class=\"day-duties\">" + dayDuties.map(function (x) { return escapeHtml(x.full_name); }).join(", ") + "</span>" : "");
calendarEl.appendChild(cell);
d.setDate(d.getDate() + 1);
}
@@ -151,13 +192,28 @@
loadingEl.classList.add("hidden");
}
async function loadMonth() {
var _initData = getInitData();
if (!_initData && !isLocalhost()) {
showAccessDenied();
return;
function runWhenReady(cb) {
if (window.Telegram && window.Telegram.WebApp) {
if (window.Telegram.WebApp.ready) {
window.Telegram.WebApp.ready();
}
if (window.Telegram.WebApp.expand) {
window.Telegram.WebApp.expand();
}
setTimeout(cb, 0);
} else {
cb();
}
}
function setNavEnabled(enabled) {
if (prevBtn) prevBtn.disabled = !enabled;
if (nextBtn) nextBtn.disabled = !enabled;
}
async function loadMonth() {
hideAccessDenied();
setNavEnabled(false);
loadingEl.classList.remove("hidden");
errorEl.hidden = true;
const from = isoDate(firstDayOfMonth(current));
@@ -170,12 +226,19 @@
} catch (e) {
if (e.message === "ACCESS_DENIED") {
showAccessDenied();
setNavEnabled(true);
if (window.Telegram && window.Telegram.WebApp && !window._initDataRetried) {
window._initDataRetried = true;
setTimeout(loadMonth, 1200);
}
return;
}
showError(e.message || "Не удалось загрузить данные.");
setNavEnabled(true);
return;
}
loadingEl.classList.add("hidden");
setNavEnabled(true);
}
document.getElementById("prevMonth").addEventListener("click", function () {
@@ -187,5 +250,5 @@
loadMonth();
});
loadMonth();
runWhenReady(loadMonth);
})();

View File

@@ -23,6 +23,7 @@
<div class="access-denied" id="accessDenied" hidden>
<p>Доступ запрещён.</p>
<p class="muted">Откройте календарь из Telegram.</p>
<p class="debug" id="accessDeniedDebug"></p>
</div>
</div>
<script src="https://telegram.org/js/telegram-web-app.js"></script>

View File

@@ -166,6 +166,12 @@ body {
font-weight: 600;
}
.access-denied .debug {
font-size: 0.75rem;
margin-top: 12px;
word-break: break-all;
}
.access-denied[hidden] {
display: none !important;
}