homelab/services/mtproto_page/xk9m2p4q7/assets/app.js
2026-05-02 18:25:46 +03:00

144 lines
4.2 KiB
JavaScript

document.getElementById('yr').textContent = new Date().getFullYear();
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);
});
}