Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
@@ -0,0 +1,404 @@
import { initThemePicker, initHighContrastToggle, initThemeToggle, applyTheme, getTheme } from "/static/js/wesp-theme.js";
import {
DEFAULT_PASSWORD_MIN_LEN,
validateCredentialChange,
} from "/static/js/wesp-user-credentials.js";
export function createRecipesAuthSettingsController({ notyf }) {
let settingsModalClickHandler = null;
let settingsEscapeHandler = null;
let syncDispenserNames = [];
let passwordMinLen = DEFAULT_PASSWORD_MIN_LEN;
function escapeHtmlSyncSettings(value) {
if (value == null) return "";
const div = document.createElement("div");
div.textContent = String(value);
return div.innerHTML;
}
function closeSettings() {
const modal = document.getElementById("settingsModal");
if (!modal) return;
modal.style.display = "none";
document.body.style.overflow = "";
const form = document.getElementById("settingsForm");
if (form) form.reset();
const credForm = document.getElementById("credentialsForm");
if (credForm) credForm.style.display = "none";
const userCard = document.querySelector(".settings-user-card--clickable");
if (userCard) userCard.classList.remove("open");
const syncForm = document.getElementById("syncSettingsForm");
if (syncForm) syncForm.style.display = "none";
const syncCard = document.querySelector(".settings-sync-card--clickable");
if (syncCard) syncCard.classList.remove("open");
if (settingsModalClickHandler) {
modal.removeEventListener("click", settingsModalClickHandler);
settingsModalClickHandler = null;
}
if (settingsEscapeHandler) {
document.removeEventListener("keydown", settingsEscapeHandler);
settingsEscapeHandler = null;
}
}
async function openSettings(event) {
if (event) event.preventDefault();
try {
const response = await fetch("/api/auth/get_current_credentials");
const data = await response.json();
if (data.status === "success") {
const oldLogin = document.getElementById("oldLogin");
const oldPassword = document.getElementById("oldPassword");
if (oldLogin) oldLogin.value = data.login || "";
if (oldPassword) oldPassword.value = "";
}
} catch (error) {
console.error("Ошибка загрузки текущих учетных данных:", error);
}
const modal = document.getElementById("settingsModal");
if (!modal) return;
modal.style.display = "block";
document.body.style.overflow = "hidden";
initThemePicker();
initHighContrastToggle();
if (settingsModalClickHandler) {
modal.removeEventListener("click", settingsModalClickHandler);
}
if (settingsEscapeHandler) {
document.removeEventListener("keydown", settingsEscapeHandler);
}
settingsModalClickHandler = (e) => {
if (e.target === modal) closeSettings();
};
settingsEscapeHandler = (e) => {
if (e.key === "Escape" && modal.style.display === "block") closeSettings();
};
modal.addEventListener("click", settingsModalClickHandler);
document.addEventListener("keydown", settingsEscapeHandler);
}
async function checkAuth() {
try {
const response = await fetch("/api/auth/check");
const data = await response.json();
if (data.status === "success" && data.authenticated) {
const userInfo = document.getElementById("userInfo");
const userLogin = document.getElementById("userLogin");
const mobileUserInfo = document.getElementById("mobileUserInfo");
const canOpenAdmin = Boolean(data.is_superuser);
window.WESP_CAN_LAB = Boolean(data.can_lab);
window.WespLabAccess?.apply?.(window.WESP_CAN_LAB);
const rules = data.user_rules || {};
passwordMinLen = Number(rules.password_min_len) || DEFAULT_PASSWORD_MIN_LEN;
if (userLogin) userLogin.textContent = data.user_login;
if (userInfo) {
userInfo.style.cursor = canOpenAdmin ? "pointer" : "";
userInfo.title = canOpenAdmin ? "Открыть админ-панель" : "";
userInfo.onclick = canOpenAdmin
? () => {
window.location.href = "/admin";
}
: null;
}
if (mobileUserInfo) {
mobileUserInfo.innerHTML = `<i class="fas fa-user"></i> ${data.user_login}`;
mobileUserInfo.style.cursor = canOpenAdmin ? "pointer" : "";
mobileUserInfo.title = canOpenAdmin ? "Открыть админ-панель" : "";
mobileUserInfo.onclick = canOpenAdmin
? () => {
window.location.href = "/admin";
}
: null;
}
} else {
window.location.href = "/login";
}
return data;
} catch (error) {
console.error("Ошибка проверки авторизации:", error);
window.location.href = "/login";
return null;
}
}
async function logout() {
try {
const response = await fetch("/api/auth/logout", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
const data = await response.json();
if (data.status === "success") {
window.location.href = "/";
} else {
notyf.error("Ошибка при выходе из системы");
}
} catch (error) {
console.error("Ошибка при выходе:", error);
notyf.error("Ошибка при выходе из системы");
}
}
function setupMobileMenu() {
const hamburgerMenu = document.getElementById("hamburgerMenu");
const mobileMenuOverlay = document.getElementById("mobileMenuOverlay");
const closeMenu = document.getElementById("closeMenu");
const mobileUserInfo = document.getElementById("mobileUserInfo");
if (!hamburgerMenu || !mobileMenuOverlay || !closeMenu) return;
hamburgerMenu.addEventListener("click", () => {
mobileMenuOverlay.classList.add("active");
hamburgerMenu.classList.add("active");
document.body.style.overflow = "hidden";
const userInfo = document.getElementById("userInfo");
if (userInfo && mobileUserInfo) {
mobileUserInfo.textContent = userInfo.textContent || "";
}
});
const closeMobileMenu = () => {
mobileMenuOverlay.classList.remove("active");
hamburgerMenu.classList.remove("active");
document.body.style.overflow = "";
};
closeMenu.addEventListener("click", closeMobileMenu);
mobileMenuOverlay.addEventListener("click", (e) => {
if (e.target === mobileMenuOverlay) closeMobileMenu();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && mobileMenuOverlay.classList.contains("active")) {
closeMobileMenu();
}
});
const mobileMenuItems = document.querySelectorAll(".mobile-menu-item");
mobileMenuItems.forEach((item) => {
item.addEventListener("click", () => {
setTimeout(closeMobileMenu, 300);
});
});
}
async function loadSyncClientsSettings() {
const listEl = document.getElementById("syncClientsListSettings");
const emptyEl = document.getElementById("syncClientsEmptySettings");
if (!listEl) return;
listEl.innerHTML = "";
if (emptyEl) emptyEl.style.display = "none";
try {
const [clientsRes, namesRes] = await Promise.all([
fetch("/api/sync/clients"),
fetch("/api/feed_dispensers/names"),
]);
const clients = clientsRes.ok ? await clientsRes.json() : [];
syncDispenserNames = namesRes.ok ? await namesRes.json() : [];
if (!clients || clients.length === 0) {
if (emptyEl) {
emptyEl.style.display = "block";
emptyEl.textContent = "Нет подключенных клиентов";
}
return;
}
clients.forEach((client) => {
const displayLabel = client.display_name || client.client_name || client.node_id;
const item = document.createElement("div");
item.className = "settings-sync-client-item";
item.dataset.nodeId = client.node_id;
item.dataset.displayName = displayLabel || "";
item.innerHTML =
`<span class="settings-sync-client-name">${escapeHtmlSyncSettings(displayLabel)}</span>` +
'<div class="settings-sync-client-actions">' +
'<button type="button" class="btn btn-sm btn-edit btn-outline-primary" data-action="sync-edit-client"><i class="fas fa-edit"></i> Изменить</button> ' +
'<button type="button" class="btn btn-sm btn-delete btn-outline-danger" data-action="sync-delete-client"><i class="fas fa-trash"></i> Удалить</button>' +
"</div>";
listEl.appendChild(item);
});
} catch (error) {
if (emptyEl) {
emptyEl.style.display = "block";
emptyEl.textContent = "Не удалось загрузить список клиентов";
}
}
}
function editSyncClientSettings(btn) {
const item = btn && btn.closest ? btn.closest(".settings-sync-client-item") : null;
const nodeId = item ? item.dataset.nodeId : null;
const currentName = item ? item.dataset.displayName || "" : "";
if (!item || !nodeId) return;
let selectHtml = '<option value="">— не задано —</option>';
syncDispenserNames.forEach((name) => {
const escaped = escapeHtmlSyncSettings(name);
selectHtml += `<option value="${escaped}">${escaped}</option>`;
});
item.innerHTML =
'<div class="settings-sync-edit-row w-100">' +
'<label class="mb-0 me-2">Имя клиента:</label>' +
`<select class="form-control form-control-sm d-inline-block" id="syncEditSelect_${nodeId}">${selectHtml}</select> ` +
`<button type="button" class="btn btn-sm btn-primary" data-action="sync-save-client" data-node-id="${String(nodeId).replace(/"/g, "&quot;")}"><i class="fas fa-save"></i> Сохранить</button> ` +
'<button type="button" class="btn btn-sm btn-secondary" data-action="sync-cancel-edit">Отмена</button>' +
"</div>";
const sel = document.getElementById(`syncEditSelect_${nodeId}`);
if (sel && currentName) sel.value = currentName;
}
function saveSyncClientDisplayName(nodeId) {
const sel = document.getElementById(`syncEditSelect_${nodeId}`);
const displayName = sel ? (sel.value || "").trim() : "";
fetch(`/api/sync/clients/${encodeURIComponent(nodeId)}/display_name`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ display_name: displayName || null }),
})
.then((r) => r.json())
.then((data) => {
if (data.error) {
notyf.error(data.error);
return;
}
notyf.success(displayName ? "Имя сохранено" : "Имя сброшено");
loadSyncClientsSettings();
})
.catch(() => notyf.error("Не удалось сохранить настройки клиента"));
}
async function deleteSyncClientSettings(btn) {
const item = btn && btn.closest ? btn.closest(".settings-sync-client-item") : null;
const nodeId = item ? item.dataset.nodeId : null;
if (!nodeId) return;
const ok = globalThis.WespKioskDialog
? await globalThis.WespKioskDialog.confirm("Удалить этого клиента из синхронизации?")
: globalThis.confirm("Удалить этого клиента из синхронизации?");
if (!ok) return;
fetch(`/api/sync/clients/${encodeURIComponent(nodeId)}`, { method: "DELETE" })
.then((r) => r.json())
.then((data) => {
if (data.error) {
notyf.error(data.error);
return;
}
notyf.success(data.message || "Клиент удалён");
loadSyncClientsSettings();
})
.catch(() => notyf.error("Не удалось сохранить настройки клиента"));
}
function toggleCredentialsForm() {
const credForm = document.getElementById("credentialsForm");
const userCard = document.querySelector(".settings-user-card--clickable");
if (!credForm) return;
const isOpen = credForm.style.display !== "none";
credForm.style.display = isOpen ? "none" : "block";
if (userCard) userCard.classList.toggle("open", !isOpen);
}
function toggleSyncForm() {
const syncForm = document.getElementById("syncSettingsForm");
const syncCard = document.querySelector(".settings-sync-card--clickable");
if (!syncForm) return;
const isOpen = syncForm.style.display !== "none";
syncForm.style.display = isOpen ? "none" : "block";
if (syncCard) syncCard.classList.toggle("open", !isOpen);
if (!isOpen) loadSyncClientsSettings();
}
async function changeCredentials() {
const oldLogin = (document.getElementById("oldLogin")?.value || "").trim();
const oldPassword = document.getElementById("oldPassword")?.value || "";
const newLogin = (document.getElementById("newLogin")?.value || "").trim();
const newPassword = document.getElementById("newPassword")?.value || "";
const confirmPassword = document.getElementById("confirmPassword")?.value || "";
const wantsCredChange = !!(newLogin || newPassword || confirmPassword.trim());
if (!wantsCredChange) {
notyf.success("Настройки сохранены");
closeSettings();
return;
}
const validationError = validateCredentialChange({
oldLogin,
oldPassword,
newLogin,
newPassword,
confirmPassword,
passwordMinLen,
});
if (validationError) {
notyf.error(validationError);
return;
}
try {
const response = await fetch("/api/auth/change_credentials", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
old_login: oldLogin,
old_password: oldPassword,
new_login: newLogin,
new_password: newPassword,
confirm_password: confirmPassword,
}),
});
const data = await response.json();
if (data.status === "success") {
notyf.success(data.message);
closeSettings();
const userLogin = document.getElementById("userLogin");
const mobileUserInfo = document.getElementById("mobileUserInfo");
if (userLogin) userLogin.textContent = newLogin;
if (mobileUserInfo) {
mobileUserInfo.innerHTML = `<i class="fas fa-user"></i> ${newLogin}`;
}
} else {
notyf.error(data.message);
}
} catch (error) {
console.error("Ошибка при изменении учетных данных:", error);
notyf.error("Ошибка при изменении учетных данных");
}
}
applyTheme(getTheme());
return {
checkAuth,
logout,
setupMobileMenu,
openSettings,
closeSettings,
toggleSyncForm,
loadSyncClientsSettings,
editSyncClientSettings,
saveSyncClientDisplayName,
deleteSyncClientSettings,
toggleCredentialsForm,
changeCredentials,
initThemePicker,
initHighContrastToggle,
initThemeToggle,
};
}