$(document).ready(() => { const token = StorageHelper.get("access_token"); if (!token) { window.location.href = "/auth"; return; } let currentUsername = ""; let currentRecoveryCodes = []; loadProfile(); function loadProfile() { Promise.all([ Api.get("/api/auth/me"), Api.get("/api/auth/roles").catch(() => ({ roles: [] })), Api.get("/api/auth/recovery-codes/status").catch(() => null), ]) .then(async ([user, rolesData, recoveryStatus]) => { document.title = `LiB - ${user.full_name || user.username}`; currentUsername = user.username; await renderProfileHeader(user); renderInfo(user); renderRoles(user.roles || [], rolesData.roles || []); window.dispatchEvent( new CustomEvent("update-2fa", { detail: user.is_2fa_enabled }), ); if (recoveryStatus) { window.dispatchEvent( new CustomEvent("update-recovery-codes", { detail: recoveryStatus.remaining, }), ); } $("#account-section, #roles-section").removeClass("hidden"); }) .catch((error) => { console.error(error); Utils.showToast("Ошибка загрузки профиля", "error"); }); } async function renderProfileHeader(user) { const avatarUrl = await Utils.getGravatarUrl(user.email); const displayName = Utils.escapeHtml(user.full_name || user.username); $("#profile-card").html(`
${user.is_verified ? '
' : ""}

${displayName}

@${Utils.escapeHtml(user.username)}

${user.is_active ? "Активен" : "Заблокирован"}
`); } function renderInfo(user) { const fields = [ { label: "ID пользователя", value: user.id }, { label: "Email", value: user.email }, { label: "Полное имя", value: user.full_name || "Не указано" }, ]; const html = fields .map( (f) => `
${f.label} ${Utils.escapeHtml(String(f.value))}
`, ) .join(""); $("#account-info").html(html); } function renderRoles(userRoles, allRoles) { const $container = $("#roles-container"); if (userRoles.length === 0) { $container.html('

Нет ролей

'); return; } const roleMap = {}; allRoles.forEach((r) => (roleMap[r.name] = r.description)); const html = userRoles .map( (role) => `
${Utils.escapeHtml(role)}
${Utils.escapeHtml(roleMap[role] || "")}
`, ) .join(""); $container.html(html); } $("#recovery-codes-btn").on("click", function () { resetRecoveryCodesModal(); window.dispatchEvent(new CustomEvent("open-recovery-codes-modal")); loadRecoveryCodesStatus(); }); function resetRecoveryCodesModal() { $("#recovery-codes-loading").removeClass("hidden"); $("#recovery-codes-status").addClass("hidden"); $("#recovery-codes-display").addClass("hidden"); $("#codes-saved-checkbox").prop("checked", false); $("#close-recovery-modal-btn").prop("disabled", true); $("#regenerate-codes-btn") .prop("disabled", false) .text("Сгенерировать новые коды"); currentRecoveryCodes = []; } async function loadRecoveryCodesStatus() { try { const status = await Api.get("/api/auth/recovery-codes/status"); renderRecoveryCodesStatus(status); } catch (error) { Utils.showToast( error.message || "Ошибка загрузки статуса кодов", "error", ); window.dispatchEvent(new CustomEvent("close-recovery-codes-modal")); } } function renderRecoveryCodesStatus(status) { const { total, remaining, used_codes, generated_at, should_regenerate } = status; let iconBgClass, iconColorClass, iconSvg; if (remaining <= 2) { iconBgClass = "bg-red-100"; iconColorClass = "text-red-600"; iconSvg = ``; } else if (remaining <= 5) { iconBgClass = "bg-yellow-100"; iconColorClass = "text-yellow-600"; iconSvg = ``; } else { iconBgClass = "bg-green-100"; iconColorClass = "text-green-600"; iconSvg = ``; } $("#status-icon-container") .removeClass() .addClass( `flex items-center justify-center w-12 h-12 mx-auto rounded-full mb-4 ${iconBgClass}`, ) .html( `${iconSvg}`, ); let statusColorClass; if (remaining <= 2) { statusColorClass = "text-red-600"; } else if (remaining <= 5) { statusColorClass = "text-yellow-600"; } else { statusColorClass = "text-green-600"; } $("#codes-status-summary").html(`

Доступно кодов: ${remaining} из ${total}

`); const $list = $("#codes-status-list"); $list.empty(); used_codes.forEach((used, index) => { const codeDisplay = "████-████-████-████"; const statusClass = used ? "text-gray-300 line-through" : "text-green-600"; const statusIcon = used ? "✗" : "✓"; const bgClass = used ? "bg-gray-50" : "bg-green-50"; $list.append(`
${index + 1}. ${codeDisplay} ${statusIcon}
`); }); if (should_regenerate || remaining <= 2) { let warningText; if (remaining === 0) { warningText = "У вас не осталось резервных кодов! Срочно сгенерируйте новые."; } else if (remaining <= 2) { warningText = "Осталось мало кодов. Рекомендуем сгенерировать новые."; } else { warningText = "Рекомендуем сгенерировать новые коды для безопасности."; } $("#warning-text").text(warningText); $("#codes-warning").removeClass("hidden"); } else { $("#codes-warning").addClass("hidden"); } if (generated_at) { const date = new Date(generated_at); $("#codes-generated-at").text(`Сгенерированы: ${date.toLocaleString()}`); } $("#recovery-codes-loading").addClass("hidden"); $("#recovery-codes-status").removeClass("hidden"); } $("#regenerate-codes-btn").on("click", async function () { const $btn = $(this); $btn.prop("disabled", true).text("Генерация..."); try { const response = await Api.post("/api/auth/recovery-codes/regenerate"); currentRecoveryCodes = response.codes; displayNewRecoveryCodes(response.codes, response.generated_at); window.dispatchEvent( new CustomEvent("update-recovery-codes", { detail: response.codes.length, }), ); Utils.showToast("Новые коды успешно сгенерированы", "success"); } catch (error) { Utils.showToast(error.message || "Ошибка генерации кодов", "error"); $btn.prop("disabled", false).text("Сгенерировать новые коды"); } }); function displayNewRecoveryCodes(codes, generatedAt) { const $list = $("#recovery-codes-list"); $list.empty(); codes.forEach((code, index) => { $list.append(`
${index + 1}. ${Utils.escapeHtml(code)}
`); }); if (generatedAt) { const date = new Date(generatedAt); $("#recovery-codes-generated-at").text( `Сгенерированы: ${date.toLocaleString()}`, ); } $("#recovery-codes-status").addClass("hidden"); $("#recovery-codes-display").removeClass("hidden"); } $("#codes-saved-checkbox").on("change", function () { $("#close-recovery-modal-btn").prop("disabled", !this.checked); }); $("#copy-codes-btn").on("click", function () { if (currentRecoveryCodes.length === 0) return; const codesText = currentRecoveryCodes.join("\n"); navigator.clipboard.writeText(codesText).then(() => { const $btn = $(this); const originalHtml = $btn.html(); $btn.html(` Скопировано! `); setTimeout(() => $btn.html(originalHtml), 2000); Utils.showToast("Коды скопированы в буфер обмена", "success"); }); }); $("#download-codes-btn").on("click", function () { if (currentRecoveryCodes.length === 0) return; const username = currentUsername || "user"; const codesText = `Резервные коды для аккаунта: ${username} Дата: ${new Date().toLocaleString()} ${currentRecoveryCodes.map((c, i) => `${i + 1}. ${c}`).join("\n")} Храните эти коды в надёжном месте! Каждый код можно использовать только один раз.`; const blob = new Blob([codesText], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `recovery-codes-${username}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Utils.showToast("Файл с кодами скачан", "success"); }); $("#close-recovery-modal-btn, #close-status-modal-btn").on( "click", function () { window.dispatchEvent(new CustomEvent("close-recovery-codes-modal")); }, ); $("#submit-disable-2fa-btn").on("click", async function () { const $btn = $(this); const password = $("#disable-2fa-password").val(); if (!password) { Utils.showToast("Введите пароль", "error"); return; } $btn.prop("disabled", true).text("Отключение..."); try { await Api.post("/api/auth/2fa/disable", { password }); Utils.showToast("2FA успешно отключена", "success"); window.dispatchEvent(new CustomEvent("update-2fa", { detail: false })); window.dispatchEvent(new CustomEvent("close-disable-2fa-modal")); $("#disable-2fa-form")[0].reset(); } catch (error) { Utils.showToast(error.message || "Ошибка отключения 2FA", "error"); } finally { $btn.prop("disabled", false).text("Отключить"); } }); $("#submit-password-btn").on("click", async function () { const $btn = $(this); const newPass = $("#new-password").val(); const confirm = $("#confirm-password").val(); if (newPass !== confirm) { Utils.showToast("Пароли не совпадают", "error"); return; } if (newPass.length < 8) { Utils.showToast("Пароль должен быть минимум 8 символов", "error"); return; } $btn.prop("disabled", true).text("Сохранение..."); try { await Api.put("/api/auth/me", { password: newPass }); Utils.showToast("Пароль успешно изменён", "success"); window.dispatchEvent(new CustomEvent("close-password-modal")); $("#change-password-form")[0].reset(); } catch (error) { Utils.showToast(error.message || "Ошибка смены пароля", "error"); } finally { $btn.prop("disabled", false).text("Сменить"); } }); });