347 lines
16 KiB
HTML
347 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Генератор паролей — WA Dev Tools</title>
|
|
<script src="/vendor/tailwind.js"></script>
|
|
<script src="/shared.js?v=3"></script>
|
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
|
<link href="/shared.css" rel="stylesheet">
|
|
<script>window.WA_TOOL_ID = 'password';</script>
|
|
<style>
|
|
select, select option { background: var(--select-bg); color: var(--select-color); }
|
|
|
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
|
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
|
|
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
|
|
|
|
/* Toggle switch */
|
|
.toggle { position: relative; width: 40px; height: 22px; border-radius: 11px; cursor: pointer; transition: background 0.2s; flex-shrink: 0; }
|
|
.toggle.off { background: var(--surface-600); }
|
|
.toggle.on { background: #0054e6; }
|
|
.toggle-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
|
|
.toggle.on .toggle-knob { left: 20px; }
|
|
|
|
/* Strength bar */
|
|
.strength-bar { height: 6px; border-radius: 3px; background: var(--surface-700); overflow: hidden; }
|
|
.strength-bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s, background 0.4s; }
|
|
|
|
/* Password display */
|
|
.pw-display {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
word-break: break-all;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Range slider */
|
|
input[type="range"] {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 100%;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background: var(--surface-700);
|
|
outline: none;
|
|
}
|
|
input[type="range"]::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: #0054e6;
|
|
cursor: pointer;
|
|
box-shadow: 0 1px 4px rgba(0,84,230,0.3);
|
|
}
|
|
input[type="range"]::-moz-range-thumb {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: #0054e6;
|
|
cursor: pointer;
|
|
border: none;
|
|
box-shadow: 0 1px 4px rgba(0,84,230,0.3);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
|
|
|
<div class="main-content">
|
|
<div class="tool-container relative z-10 max-w-2xl mx-auto py-8 sm:py-12">
|
|
|
|
<!-- Header -->
|
|
<div class="text-center mb-8 fade-up">
|
|
<div class="page-header" style="text-align:center;">
|
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
|
A-Z · a-z · 0-9 · !@#$
|
|
</div>
|
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
|
Генератор <span class="text-accent">паролей</span>
|
|
</h1>
|
|
<p class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">Криптографически стойкая генерация паролей любой сложности</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Card -->
|
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
|
|
|
<!-- Length -->
|
|
<div class="mb-5">
|
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-2 font-mono uppercase tracking-wider">Длина</label>
|
|
<div class="flex items-center gap-4">
|
|
<input type="range" id="lengthRange" min="4" max="128" value="16" class="flex-1">
|
|
<input type="number" id="lengthInput" min="4" max="128" value="16"
|
|
class="w-20 px-3 py-2 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm font-mono text-center focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toggles -->
|
|
<div class="space-y-3 mb-5">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm dark:text-gray-300 text-gray-600">Верхний регистр <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(A-Z)</span></span>
|
|
<div class="toggle on" data-opt="upper"><div class="toggle-knob"></div></div>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm dark:text-gray-300 text-gray-600">Нижний регистр <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(a-z)</span></span>
|
|
<div class="toggle on" data-opt="lower"><div class="toggle-knob"></div></div>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm dark:text-gray-300 text-gray-600">Цифры <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(0-9)</span></span>
|
|
<div class="toggle on" data-opt="digits"><div class="toggle-knob"></div></div>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm dark:text-gray-300 text-gray-600">Символы <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(!@#$%^&*...)</span></span>
|
|
<div class="toggle off" data-opt="symbols"><div class="toggle-knob"></div></div>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm dark:text-gray-300 text-gray-600">Исключить похожие <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(l, 1, I, O, 0, o)</span></span>
|
|
<div class="toggle off" data-opt="exclude"><div class="toggle-knob"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Count -->
|
|
<div class="mb-5">
|
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Количество</label>
|
|
<select id="countSelect" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm focus:outline-none focus:border-accent/50 transition-colors">
|
|
<option value="1">1 пароль</option>
|
|
<option value="5">5 паролей</option>
|
|
<option value="10">10 паролей</option>
|
|
<option value="25">25 паролей</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Generate button -->
|
|
<button id="generateBtn"
|
|
class="w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
|
bg-accent text-white border border-accent cursor-pointer
|
|
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20
|
|
active:scale-[0.98]">
|
|
Сгенерировать
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Result -->
|
|
<div id="resultCard" class="mt-5 fade-up">
|
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
|
|
|
<!-- Strength indicator -->
|
|
<div id="strengthBlock" class="mb-5 hidden">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span id="strengthLabel" class="text-xs font-medium font-mono uppercase tracking-wider"></span>
|
|
<span id="entropyLabel" class="text-xs font-mono dark:text-gray-500 text-gray-400"></span>
|
|
</div>
|
|
<div class="strength-bar">
|
|
<div id="strengthFill" class="strength-bar-fill" style="width:0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Single password display -->
|
|
<div id="singleResult" class="hidden">
|
|
<div class="flex items-start gap-3">
|
|
<div id="passwordDisplay" class="pw-display flex-1 text-lg sm:text-xl dark:text-white text-gray-900 dark:bg-surface-700/50 bg-gray-50 rounded-xl px-4 py-3 border dark:border-surface-600 border-gray-200 select-all"></div>
|
|
<button id="copyBtn" class="px-4 py-3 rounded-xl text-sm font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200 whitespace-nowrap flex-shrink-0">
|
|
Копировать
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Multi password display -->
|
|
<div id="multiResult" class="hidden space-y-2">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Пароли</span>
|
|
<button id="copyAllBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
|
Копировать все
|
|
</button>
|
|
</div>
|
|
<div id="passwordList"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div><!-- /main-content -->
|
|
|
|
<script>
|
|
// State
|
|
const opts = { upper: true, lower: true, digits: true, symbols: false, exclude: false };
|
|
const CHARSETS = {
|
|
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
lower: 'abcdefghijklmnopqrstuvwxyz',
|
|
digits: '0123456789',
|
|
symbols: '!@#$%^&*()-_=+[]{}|;:,.<>?/~`',
|
|
};
|
|
const SIMILAR = 'l1IO0o';
|
|
|
|
// Toggle handlers
|
|
document.querySelectorAll('.toggle[data-opt]').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
const opt = el.dataset.opt;
|
|
opts[opt] = !opts[opt];
|
|
el.classList.toggle('on', opts[opt]);
|
|
el.classList.toggle('off', !opts[opt]);
|
|
});
|
|
});
|
|
|
|
// Length sync
|
|
const lengthRange = document.getElementById('lengthRange');
|
|
const lengthInput = document.getElementById('lengthInput');
|
|
lengthRange.addEventListener('input', () => { lengthInput.value = lengthRange.value; });
|
|
lengthInput.addEventListener('input', () => {
|
|
let v = parseInt(lengthInput.value, 10);
|
|
if (v < 4) v = 4;
|
|
if (v > 128) v = 128;
|
|
lengthRange.value = v;
|
|
});
|
|
|
|
function buildCharset() {
|
|
let cs = '';
|
|
if (opts.upper) cs += CHARSETS.upper;
|
|
if (opts.lower) cs += CHARSETS.lower;
|
|
if (opts.digits) cs += CHARSETS.digits;
|
|
if (opts.symbols) cs += CHARSETS.symbols;
|
|
if (opts.exclude) {
|
|
cs = cs.split('').filter(c => !SIMILAR.includes(c)).join('');
|
|
}
|
|
return cs;
|
|
}
|
|
|
|
function generatePassword(length, charset) {
|
|
const arr = new Uint32Array(length);
|
|
crypto.getRandomValues(arr);
|
|
let pw = '';
|
|
for (let i = 0; i < length; i++) {
|
|
pw += charset[arr[i] % charset.length];
|
|
}
|
|
return pw;
|
|
}
|
|
|
|
function calcEntropy(length, charsetSize) {
|
|
if (charsetSize <= 1) return 0;
|
|
return Math.floor(length * Math.log2(charsetSize));
|
|
}
|
|
|
|
function getStrength(entropy) {
|
|
if (entropy < 28) return { label: 'Слабый', color: 'text-danger', bar: 'bg-danger', pct: 15 };
|
|
if (entropy < 36) return { label: 'Средний', color: 'text-warn', bar: 'bg-warn', pct: 35 };
|
|
if (entropy < 60) return { label: 'Сильный', color: 'text-accent/70', bar: 'bg-accent/60', pct: 65 };
|
|
return { label: 'Очень сильный', color: 'text-accent', bar: 'bg-accent', pct: 100 };
|
|
}
|
|
|
|
function showStrength(length, charsetSize) {
|
|
const block = document.getElementById('strengthBlock');
|
|
const entropy = calcEntropy(length, charsetSize);
|
|
const s = getStrength(entropy);
|
|
|
|
document.getElementById('strengthLabel').textContent = s.label;
|
|
document.getElementById('strengthLabel').className = 'text-xs font-medium font-mono uppercase tracking-wider ' + s.color;
|
|
document.getElementById('entropyLabel').textContent = entropy + ' бит энтропии';
|
|
const fill = document.getElementById('strengthFill');
|
|
fill.className = 'strength-bar-fill ' + s.bar;
|
|
fill.style.width = s.pct + '%';
|
|
block.classList.remove('hidden');
|
|
}
|
|
|
|
function copyText(text, btn) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const orig = btn.textContent;
|
|
btn.textContent = 'Скопировано!';
|
|
btn.classList.add('text-accent');
|
|
setTimeout(() => { btn.textContent = orig; btn.classList.remove('text-accent'); }, 1500);
|
|
});
|
|
}
|
|
|
|
document.getElementById('generateBtn').addEventListener('click', generate);
|
|
|
|
function generate() {
|
|
const charset = buildCharset();
|
|
if (!charset) {
|
|
alert('Выберите хотя бы один набор символов');
|
|
return;
|
|
}
|
|
|
|
const length = parseInt(lengthInput.value, 10) || 16;
|
|
const count = parseInt(document.getElementById('countSelect').value, 10);
|
|
|
|
showStrength(length, charset.length);
|
|
|
|
const singleResult = document.getElementById('singleResult');
|
|
const multiResult = document.getElementById('multiResult');
|
|
|
|
if (count === 1) {
|
|
const pw = generatePassword(length, charset);
|
|
document.getElementById('passwordDisplay').textContent = pw;
|
|
singleResult.classList.remove('hidden');
|
|
multiResult.classList.add('hidden');
|
|
|
|
document.getElementById('copyBtn').onclick = () => copyText(pw, document.getElementById('copyBtn'));
|
|
} else {
|
|
const passwords = [];
|
|
for (let i = 0; i < count; i++) {
|
|
passwords.push(generatePassword(length, charset));
|
|
}
|
|
|
|
const list = document.getElementById('passwordList');
|
|
list.innerHTML = '';
|
|
passwords.forEach((pw, i) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'flex items-center gap-2 py-2' + (i > 0 ? ' border-t dark:border-surface-600/50 border-gray-100' : '');
|
|
row.innerHTML = `
|
|
<span class="pw-display flex-1 text-sm dark:text-gray-200 text-gray-700 select-all break-all">${escapeHtml(pw)}</span>
|
|
<button class="copy-row-btn px-3 py-1.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-400 text-gray-500 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200 whitespace-nowrap flex-shrink-0">Копировать</button>
|
|
`;
|
|
row.querySelector('.copy-row-btn').addEventListener('click', function() { copyText(pw, this); });
|
|
list.appendChild(row);
|
|
});
|
|
|
|
singleResult.classList.add('hidden');
|
|
multiResult.classList.remove('hidden');
|
|
|
|
document.getElementById('copyAllBtn').onclick = () => copyText(passwords.join('\n'), document.getElementById('copyAllBtn'));
|
|
}
|
|
|
|
document.getElementById('resultCard').classList.remove('hidden');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// Generate on load
|
|
generate();
|
|
</script>
|
|
<!-- Yandex.Metrika counter -->
|
|
<script type="text/javascript">
|
|
(function(m,e,t,r,i,k,a){
|
|
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
|
m[i].l=1*new Date();
|
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
|
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
|
|
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
|
</script>
|
|
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
|
<!-- /Yandex.Metrika counter -->
|
|
</body>
|
|
</html>
|