Files
duty-teller/webapp/app.js
Nikolay Tatarinov 5dc8c8f255 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.
2026-02-17 17:21:35 +03:00

255 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
const MONTHS = [
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"
];
let current = new Date();
const calendarEl = document.getElementById("calendar");
const monthTitleEl = document.getElementById("monthTitle");
const dutyListEl = document.getElementById("dutyList");
const loadingEl = document.getElementById("loading");
const errorEl = document.getElementById("error");
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);
}
function firstDayOfMonth(d) {
return new Date(d.getFullYear(), d.getMonth(), 1);
}
function lastDayOfMonth(d) {
return new Date(d.getFullYear(), d.getMonth() + 1, 0);
}
function getMonday(d) {
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
return new Date(d.getFullYear(), d.getMonth(), diff);
}
function getInitData() {
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() {
var h = window.location.hostname;
return h === "localhost" || h === "127.0.0.1" || h === "";
}
function showAccessDenied() {
if (headerEl) headerEl.hidden = true;
if (weekdaysEl) weekdaysEl.hidden = true;
calendarEl.hidden = true;
dutyListEl.hidden = true;
loadingEl.classList.add("hidden");
errorEl.hidden = true;
accessDeniedEl.hidden = false;
var debugEl = document.getElementById("accessDeniedDebug");
if (debugEl) debugEl.textContent = getInitDataDebug();
}
function hideAccessDenied() {
accessDeniedEl.hidden = true;
if (headerEl) headerEl.hidden = false;
if (weekdaysEl) weekdaysEl.hidden = false;
calendarEl.hidden = false;
dutyListEl.hidden = false;
}
async function fetchDuties(from, to) {
const base = window.location.origin;
const url = base + "/api/duties?from=" + encodeURIComponent(from) + "&to=" + encodeURIComponent(to);
const initData = getInitData();
const headers = {};
if (initData) headers["X-Telegram-Init-Data"] = initData;
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;
}
}
function renderCalendar(year, month, dutiesByDate) {
const first = firstDayOfMonth(new Date(year, month, 1));
const last = lastDayOfMonth(new Date(year, month, 1));
const start = getMonday(first);
const today = isoDate(new Date());
calendarEl.innerHTML = "";
let d = new Date(start);
const cells = 42;
for (let i = 0; i < cells; i++) {
const key = isoDate(d);
const isOther = d.getMonth() !== month;
const dayDuties = dutiesByDate[key] || [];
const isToday = key === today;
const cell = document.createElement("div");
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 escapeHtml(x.full_name); }).join(", ") + "</span>" : "");
calendarEl.appendChild(cell);
d.setDate(d.getDate() + 1);
}
monthTitleEl.textContent = MONTHS[month] + " " + year;
}
function renderDutyList(duties) {
if (duties.length === 0) {
dutyListEl.innerHTML = "<p class=\"muted\">В этом месяце дежурств нет.</p>";
return;
}
const grouped = {};
duties.forEach(function (d) {
const date = d.start_at.slice(0, 10);
if (!grouped[date]) grouped[date] = [];
grouped[date].push(d);
});
const dates = Object.keys(grouped).sort();
let html = "";
dates.forEach(function (date) {
const list = grouped[date];
html += "<h2>" + date + "</h2>";
list.forEach(function (d) {
const start = d.start_at.slice(11, 16);
const end = d.end_at.slice(11, 16);
html += "<div class=\"duty-item\"><span class=\"name\">" + escapeHtml(d.full_name) + "</span><div class=\"time\">" + start + " " + end + "</div></div>";
});
});
dutyListEl.innerHTML = html;
}
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
function dutiesByDate(duties) {
const byDate = {};
duties.forEach(function (d) {
const start = new Date(d.start_at);
const end = new Date(d.end_at);
for (let t = new Date(start); t <= end; t.setDate(t.getDate() + 1)) {
const key = isoDate(t);
if (!byDate[key]) byDate[key] = [];
byDate[key].push(d);
}
});
return byDate;
}
function showError(msg) {
errorEl.textContent = msg;
errorEl.hidden = false;
loadingEl.classList.add("hidden");
}
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));
const to = isoDate(lastDayOfMonth(current));
try {
const duties = await fetchDuties(from, to);
const byDate = dutiesByDate(duties);
renderCalendar(current.getFullYear(), current.getMonth(), byDate);
renderDutyList(duties);
} 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 () {
current.setMonth(current.getMonth() - 1);
loadMonth();
});
document.getElementById("nextMonth").addEventListener("click", function () {
current.setMonth(current.getMonth() + 1);
loadMonth();
});
runWhenReady(loadMonth);
})();