453 lines
15 KiB
HTML
453 lines
15 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="ru">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>Lic Decoder</title>
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--bg: #f5f5f5;
|
|||
|
|
--surface: #fff;
|
|||
|
|
--border: #ddd;
|
|||
|
|
--fg: #222;
|
|||
|
|
--fg2: #555;
|
|||
|
|
--fg3: #888;
|
|||
|
|
--accent: #2a6cb8;
|
|||
|
|
--red: #c0392b;
|
|||
|
|
--green: #27864a;
|
|||
|
|
--radius: 6px;
|
|||
|
|
}
|
|||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|||
|
|
body {
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|||
|
|
background: var(--bg);
|
|||
|
|
color: var(--fg);
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
.layout {
|
|||
|
|
max-width: 720px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
padding: 32px 20px;
|
|||
|
|
}
|
|||
|
|
h1 {
|
|||
|
|
font-size: 1.25rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
h2 {
|
|||
|
|
font-size: 1rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin: 24px 0 8px;
|
|||
|
|
color: var(--fg);
|
|||
|
|
}
|
|||
|
|
p, li {
|
|||
|
|
color: var(--fg2);
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
ul { margin: 6px 0 0 20px; }
|
|||
|
|
li { margin-bottom: 4px; }
|
|||
|
|
.card {
|
|||
|
|
background: var(--surface);
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
padding: 16px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
.path-box {
|
|||
|
|
font-family: "Cascadia Code", "Fira Code", Consolas, monospace;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
background: #f0f0f0;
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
color: var(--fg);
|
|||
|
|
display: inline-block;
|
|||
|
|
margin: 6px 0;
|
|||
|
|
}
|
|||
|
|
.status-bar {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
font-size: 0.82rem;
|
|||
|
|
color: var(--fg3);
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
.status-dot {
|
|||
|
|
width: 7px; height: 7px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: var(--fg3);
|
|||
|
|
}
|
|||
|
|
.status-dot.ok { background: var(--green); }
|
|||
|
|
.status-dot.fail { background: var(--red); }
|
|||
|
|
|
|||
|
|
.upload-zone {
|
|||
|
|
border: 2px dashed var(--border);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
padding: 32px;
|
|||
|
|
text-align: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: border-color 0.15s;
|
|||
|
|
}
|
|||
|
|
.upload-zone:hover, .upload-zone.dragover {
|
|||
|
|
border-color: var(--accent);
|
|||
|
|
}
|
|||
|
|
.upload-zone p {
|
|||
|
|
color: var(--fg3);
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
margin-top: 6px;
|
|||
|
|
}
|
|||
|
|
.upload-zone strong { color: var(--fg2); }
|
|||
|
|
#file-input { display: none; }
|
|||
|
|
|
|||
|
|
.options {
|
|||
|
|
margin-top: 12px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
}
|
|||
|
|
.options label {
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
color: var(--fg2);
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.options input[type="checkbox"] {
|
|||
|
|
accent-color: var(--accent);
|
|||
|
|
width: 15px; height: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
margin-top: 12px;
|
|||
|
|
padding: 8px 20px;
|
|||
|
|
background: var(--accent);
|
|||
|
|
color: #fff;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
font-size: 0.88rem;
|
|||
|
|
font-weight: 500;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.btn:hover { opacity: 0.9; }
|
|||
|
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|||
|
|
.btn.loading { background: var(--fg3); }
|
|||
|
|
.file-name {
|
|||
|
|
margin-left: 10px;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
color: var(--fg3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-box {
|
|||
|
|
background: rgba(192,57,43,0.06);
|
|||
|
|
border: 1px solid rgba(192,57,43,0.2);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
padding: 12px;
|
|||
|
|
color: var(--red);
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
margin-top: 12px;
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
.error-box.visible { display: block; }
|
|||
|
|
|
|||
|
|
.results {
|
|||
|
|
margin-top: 16px;
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
.results.visible { display: block; }
|
|||
|
|
.result-card {
|
|||
|
|
background: var(--surface);
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
.result-card-header {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
border-bottom: 1px solid var(--border);
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: var(--fg2);
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.03em;
|
|||
|
|
}
|
|||
|
|
.result-card-body {
|
|||
|
|
padding: 12px;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
font-family: "Cascadia Code", "Fira Code", Consolas, monospace;
|
|||
|
|
font-size: 0.82rem;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
color: var(--fg);
|
|||
|
|
}
|
|||
|
|
.result-card-body.secondary { color: var(--fg2); }
|
|||
|
|
.key-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
padding: 6px 0;
|
|||
|
|
}
|
|||
|
|
.key-label {
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.04em;
|
|||
|
|
color: var(--fg3);
|
|||
|
|
min-width: 140px;
|
|||
|
|
}
|
|||
|
|
.key-badge {
|
|||
|
|
display: inline-block;
|
|||
|
|
padding: 3px 10px;
|
|||
|
|
background: rgba(42,108,184,0.08);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-family: monospace;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
color: var(--accent);
|
|||
|
|
letter-spacing: 1px;
|
|||
|
|
}
|
|||
|
|
.keys-card {
|
|||
|
|
background: var(--surface);
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
}
|
|||
|
|
.hw-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
border-top: 1px solid var(--border);
|
|||
|
|
}
|
|||
|
|
.hw-grid .result-card {
|
|||
|
|
margin: 0;
|
|||
|
|
border-radius: 0;
|
|||
|
|
border: none;
|
|||
|
|
border-left: 1px solid var(--border);
|
|||
|
|
}
|
|||
|
|
.hw-grid .result-card:first-child { border-left: none; }
|
|||
|
|
@media (max-width: 600px) {
|
|||
|
|
.hw-grid { grid-template-columns: 1fr; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="layout">
|
|||
|
|
<h1>Lic Decoder</h1>
|
|||
|
|
|
|||
|
|
<div class="status-bar" id="status-bar">
|
|||
|
|
<div class="status-dot" id="status-dot"></div>
|
|||
|
|
<span id="status-text">Проверка...</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<h2>Описание</h2>
|
|||
|
|
<p>Утилита для извлечения регистрационных данных из файлов лицензий 1С:Предприятие (.lic). Позволяет восстановить информацию о лицензии (компания, ФИО, почта, адрес, PIN-код), если файл LicData.txt был утерян.</p>
|
|||
|
|
|
|||
|
|
<h2>Источник файлов</h2>
|
|||
|
|
<p>Файлы лицензий 1С хранятся в:</p>
|
|||
|
|
<div class="path-box">C:\ProgramData\1C\licenses</div>
|
|||
|
|
<p>Скопируйте нужный <strong>.lic</strong> файл и загрузите ниже.</p>
|
|||
|
|
|
|||
|
|
<h2>Что показывает</h2>
|
|||
|
|
<ul>
|
|||
|
|
<li>ORGANIZATION - Организация</li>
|
|||
|
|
<li>FIRST NAME / MIDDLE NAME / LAST NAME - ФИО владельца</li>
|
|||
|
|
<li>EMAIL - Электронная почта</li>
|
|||
|
|
<li>COUNTRY / REGION / CITY / STREET - Адрес регистрации</li>
|
|||
|
|
<li>PIN-CODE - PIN-код активации</li>
|
|||
|
|
</ul>
|
|||
|
|
<p style="margin-top:6px">При включении подробного режима также отображается аппаратная конфигурация лицензированной и текущей машины.</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="upload-zone" id="drop-zone">
|
|||
|
|
<strong>Перетащите .lic файл</strong>
|
|||
|
|
<p>или нажмите для выбора</p>
|
|||
|
|
<input type="file" id="file-input" accept=".lic">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="options">
|
|||
|
|
<input type="checkbox" id="detailed">
|
|||
|
|
<label for="detailed">Подробный режим (аппаратная конфигурация)</label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<button class="btn" id="decode-btn" disabled>Декодировать</button>
|
|||
|
|
<span class="file-name" id="file-name-display"></span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="error-box" class="error-box"></div>
|
|||
|
|
|
|||
|
|
<div id="results" class="results">
|
|||
|
|
<div class="keys-card" id="keys-card" style="display:none">
|
|||
|
|
<div class="key-row" id="pin-row">
|
|||
|
|
<span class="key-label">PIN-код</span>
|
|||
|
|
<span class="key-badge" id="pin-value"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="key-row" id="reg-row" style="display:none">
|
|||
|
|
<span class="key-label">Рег. Номер Комплекта</span>
|
|||
|
|
<span class="key-badge" id="reg-value"></span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="result-card">
|
|||
|
|
<div class="result-card-header">Данные лицензии</div>
|
|||
|
|
<div class="result-card-body" id="lic-data"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="result-card">
|
|||
|
|
<div class="result-card-header">Валидация</div>
|
|||
|
|
<div class="result-card-body" id="validate-data"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="result-card" id="hw-card" style="display:none">
|
|||
|
|
<div class="result-card-header">Аппаратная конфигурация</div>
|
|||
|
|
<div class="result-card-body" style="padding:0">
|
|||
|
|
<div class="hw-grid">
|
|||
|
|
<div class="result-card">
|
|||
|
|
<div class="result-card-header">Лицензированная машина</div>
|
|||
|
|
<div class="result-card-body secondary" id="lic-hw"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="result-card">
|
|||
|
|
<div class="result-card-header">Текущая машина</div>
|
|||
|
|
<div class="result-card-body secondary" id="cur-hw"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
const dropZone = document.getElementById("drop-zone");
|
|||
|
|
const fileInput = document.getElementById("file-input");
|
|||
|
|
const decodeBtn = document.getElementById("decode-btn");
|
|||
|
|
const detailedCb = document.getElementById("detailed");
|
|||
|
|
const resultsDiv = document.getElementById("results");
|
|||
|
|
const errorBox = document.getElementById("error-box");
|
|||
|
|
const statusDot = document.getElementById("status-dot");
|
|||
|
|
const statusText = document.getElementById("status-text");
|
|||
|
|
const fileNameDisplay = document.getElementById("file-name-display");
|
|||
|
|
|
|||
|
|
let selectedFile = null;
|
|||
|
|
|
|||
|
|
async function checkStatus() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch("/api/status");
|
|||
|
|
const data = await res.json();
|
|||
|
|
let ready = true;
|
|||
|
|
const parts = [];
|
|||
|
|
if (data.jre.installed) {
|
|||
|
|
if (!data.jre.valid) ready = false;
|
|||
|
|
parts.push(data.jre.valid ? `JRE ${data.jre.version}` : `JRE ${data.jre.version} (устаревшая)`);
|
|||
|
|
} else {
|
|||
|
|
ready = false;
|
|||
|
|
parts.push("JRE не найдена");
|
|||
|
|
}
|
|||
|
|
if (data.ring.installed) {
|
|||
|
|
if (!data.ring.valid) ready = false;
|
|||
|
|
parts.push(data.ring.valid ? `Ring ${data.ring.version}` : `Ring ${data.ring.version} (устаревший)`);
|
|||
|
|
} else {
|
|||
|
|
ready = false;
|
|||
|
|
parts.push("Ring не найден");
|
|||
|
|
}
|
|||
|
|
statusText.textContent = parts.join(" \u00b7 ");
|
|||
|
|
statusDot.className = "status-dot " + (ready ? "ok" : "fail");
|
|||
|
|
} catch {
|
|||
|
|
statusText.textContent = "Сервер недоступен";
|
|||
|
|
statusDot.className = "status-dot fail";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
dropZone.addEventListener("click", () => fileInput.click());
|
|||
|
|
dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("dragover"); });
|
|||
|
|
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("dragover"));
|
|||
|
|
dropZone.addEventListener("drop", (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
dropZone.classList.remove("dragover");
|
|||
|
|
if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
|
|||
|
|
});
|
|||
|
|
fileInput.addEventListener("change", () => {
|
|||
|
|
if (fileInput.files.length) handleFile(fileInput.files[0]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function handleFile(file) {
|
|||
|
|
if (!file.name.toLowerCase().endsWith(".lic")) {
|
|||
|
|
showError("Поддерживаются только файлы .lic");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
selectedFile = file;
|
|||
|
|
dropZone.querySelector("strong").textContent = file.name;
|
|||
|
|
dropZone.querySelector("p").textContent = "Нажмите, чтобы выбрать другой";
|
|||
|
|
fileNameDisplay.textContent = file.name;
|
|||
|
|
decodeBtn.disabled = false;
|
|||
|
|
resultsDiv.classList.remove("visible");
|
|||
|
|
errorBox.classList.remove("visible");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
decodeBtn.addEventListener("click", async () => {
|
|||
|
|
if (!selectedFile) return;
|
|||
|
|
decodeBtn.disabled = true;
|
|||
|
|
decodeBtn.textContent = "Декодирование...";
|
|||
|
|
decodeBtn.classList.add("loading");
|
|||
|
|
resultsDiv.classList.remove("visible");
|
|||
|
|
errorBox.classList.remove("visible");
|
|||
|
|
|
|||
|
|
const fd = new FormData();
|
|||
|
|
fd.append("license", selectedFile);
|
|||
|
|
fd.append("detailed", detailedCb.checked);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch("/api/decode", { method: "POST", body: fd });
|
|||
|
|
const data = await res.json();
|
|||
|
|
if (res.status === 429) {
|
|||
|
|
const retry = data.retryAfter ? ` Попробуйте через ${data.retryAfter}с.` : "";
|
|||
|
|
throw new Error("Лимит превышен: не более 3 декодирований в час." + retry);
|
|||
|
|
}
|
|||
|
|
if (!res.ok) throw new Error(data.error || data.details || "Декодирование не удалось");
|
|||
|
|
|
|||
|
|
let licText = data.licData;
|
|||
|
|
let regNumber = null;
|
|||
|
|
const regMatch = licText.match(/РегНомерКомплекта:\s*(.+)/);
|
|||
|
|
if (regMatch) {
|
|||
|
|
regNumber = regMatch[1].trim();
|
|||
|
|
licText = licText.replace(/РегНомерКомплекта:\s*.+\n?/, '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById("lic-data").textContent = licText.trim();
|
|||
|
|
document.getElementById("pin-value").textContent = data.pinCode;
|
|||
|
|
document.getElementById("keys-card").style.display = "";
|
|||
|
|
if (regNumber) {
|
|||
|
|
document.getElementById("reg-value").textContent = regNumber;
|
|||
|
|
document.getElementById("reg-row").style.display = "";
|
|||
|
|
} else {
|
|||
|
|
document.getElementById("reg-row").style.display = "none";
|
|||
|
|
}
|
|||
|
|
document.getElementById("validate-data").textContent = data.validateData;
|
|||
|
|
|
|||
|
|
const hwCard = document.getElementById("hw-card");
|
|||
|
|
if (data.licHWConfig || data.currentHWConfig) {
|
|||
|
|
hwCard.style.display = "";
|
|||
|
|
document.getElementById("lic-hw").textContent = data.licHWConfig || "N/A";
|
|||
|
|
document.getElementById("cur-hw").textContent = data.currentHWConfig || "N/A";
|
|||
|
|
} else {
|
|||
|
|
hwCard.style.display = "none";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resultsDiv.classList.add("visible");
|
|||
|
|
} catch (e) {
|
|||
|
|
showError(e.message);
|
|||
|
|
} finally {
|
|||
|
|
decodeBtn.disabled = false;
|
|||
|
|
decodeBtn.textContent = "Декодировать";
|
|||
|
|
decodeBtn.classList.remove("loading");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function escapeHtml(text) {
|
|||
|
|
const d = document.createElement('div');
|
|||
|
|
d.textContent = text;
|
|||
|
|
return d.innerHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showError(msg) {
|
|||
|
|
errorBox.textContent = msg;
|
|||
|
|
errorBox.classList.add("visible");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkStatus();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|