mirror of
https://github.com/pvlnes/homelab.git
synced 2026-06-03 18:13:50 +00:00
146 lines
4.4 KiB
JavaScript
146 lines
4.4 KiB
JavaScript
document.getElementById('yr').textContent = new Date().getFullYear();
|
|
|
|
document.getElementById('submit-btn').addEventListener('click', tryAuth);
|
|
document.getElementById('eye-btn').addEventListener('click', toggleVis);
|
|
document.getElementById('pass-input').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') tryAuth();
|
|
});
|
|
|
|
function toggleVis() {
|
|
const inp = document.getElementById('pass-input');
|
|
inp.type = inp.type === 'password' ? 'text' : 'password';
|
|
}
|
|
|
|
function b64ToBytes(b64) {
|
|
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
}
|
|
|
|
async function tryAuth() {
|
|
const password = document.getElementById('pass-input').value;
|
|
if (!password) return;
|
|
|
|
const btn = document.getElementById('submit-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = '...';
|
|
|
|
try {
|
|
const res = await fetch('./data.enc');
|
|
const { salt, iv, ct } = await res.json();
|
|
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(password),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveKey']
|
|
);
|
|
const key = await crypto.subtle.deriveKey(
|
|
{ name: 'PBKDF2', salt: b64ToBytes(salt), iterations: 100000, hash: 'SHA-256' },
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['decrypt']
|
|
);
|
|
|
|
// Wrong password → AES-GCM auth tag mismatch → throws → catch below
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv: b64ToBytes(iv) },
|
|
key,
|
|
b64ToBytes(ct)
|
|
);
|
|
|
|
const proxies = JSON.parse(new TextDecoder().decode(decrypted));
|
|
document.getElementById('auth-wall').style.display = 'none';
|
|
renderProxies(proxies);
|
|
const list = document.getElementById('proxy-list');
|
|
list.style.display = 'flex';
|
|
list.style.flexDirection = 'column';
|
|
|
|
} catch {
|
|
showError();
|
|
btn.disabled = false;
|
|
btn.textContent = '→ Нажать';
|
|
}
|
|
}
|
|
|
|
function showError() {
|
|
const input = document.getElementById('pass-input');
|
|
const msg = document.getElementById('err-msg');
|
|
input.classList.add('error');
|
|
msg.classList.add('show');
|
|
input.animate(
|
|
[{ transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(0)' }],
|
|
{ duration: 200, iterations: 3 }
|
|
);
|
|
setTimeout(() => { input.classList.remove('error'); msg.classList.remove('show'); }, 3000);
|
|
}
|
|
|
|
function tgUrl(p) {
|
|
return `tg://proxy?server=${encodeURIComponent(p.server)}&port=${encodeURIComponent(p.port)}&secret=${encodeURIComponent(p.secret)}`;
|
|
}
|
|
|
|
function renderProxies(proxies) {
|
|
const list = document.getElementById('proxy-list');
|
|
proxies.forEach((p, i) => {
|
|
const url = tgUrl(p);
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'proxy-card';
|
|
|
|
const hdr = document.createElement('div');
|
|
hdr.className = 'card-header';
|
|
|
|
const idx = document.createElement('span');
|
|
idx.className = 'node-index';
|
|
idx.textContent = `NODE ${String(i + 1).padStart(2, '0')}`;
|
|
|
|
const name = document.createElement('span');
|
|
name.className = 'node-name';
|
|
name.textContent = p.name;
|
|
|
|
const flag = document.createElement('span');
|
|
flag.className = 'node-flag';
|
|
flag.textContent = p.flag;
|
|
|
|
hdr.append(idx, name, flag);
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'card-meta';
|
|
[['server', p.server], ['port', p.port], ['secret', p.secret]].forEach(([k, v]) => {
|
|
const key = document.createElement('span');
|
|
key.className = 'key';
|
|
key.textContent = k;
|
|
const val = document.createElement('span');
|
|
val.className = 'val';
|
|
val.textContent = v;
|
|
meta.append(key, val);
|
|
});
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'card-actions';
|
|
|
|
const tgBtn = document.createElement('a');
|
|
tgBtn.className = 'btn-tg';
|
|
tgBtn.href = url;
|
|
tgBtn.textContent = '✈ Open in Telegram';
|
|
|
|
const copyBtn = document.createElement('button');
|
|
copyBtn.className = 'btn-copy';
|
|
copyBtn.textContent = '⎘ Copy link';
|
|
copyBtn.addEventListener('click', () => {
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
copyBtn.textContent = '✓ Copied';
|
|
copyBtn.classList.add('copied');
|
|
setTimeout(() => {
|
|
copyBtn.textContent = '⎘ Copy link';
|
|
copyBtn.classList.remove('copied');
|
|
}, 2000);
|
|
});
|
|
});
|
|
|
|
actions.append(tgBtn, copyBtn);
|
|
card.append(hdr, meta, actions);
|
|
list.appendChild(card);
|
|
});
|
|
}
|