282 lines
40 KiB
JavaScript
282 lines
40 KiB
JavaScript
'use strict';
|
||
|
||
const $ = s => document.querySelector(s);
|
||
const $$ = s => [...document.querySelectorAll(s)];
|
||
let apps = [];
|
||
let clipStream = null;
|
||
let toastTimer = null;
|
||
let csvRows = [];
|
||
let csvCurrentDelimiter = ';';
|
||
let lastFileSHA256 = '';
|
||
|
||
function toast(msg) {
|
||
const el = $('#toast');
|
||
el.textContent = msg;
|
||
el.classList.add('show');
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => el.classList.remove('show'), 2600);
|
||
}
|
||
|
||
async function api(url, options = {}) {
|
||
const res = await fetch(url, options);
|
||
if (!res.ok) {
|
||
let msg = `HTTP ${res.status}`;
|
||
try { const j = await res.json(); if (j.error) msg = j.error; } catch (_) {}
|
||
throw new Error(msg);
|
||
}
|
||
const ct = res.headers.get('content-type') || '';
|
||
return ct.includes('application/json') ? res.json() : res.text();
|
||
}
|
||
function fmtBytes(n) {
|
||
const u = ['B','KB','MB','GB','TB']; let i=0, v=Number(n||0);
|
||
while (v >= 1024 && i < u.length-1) { v/=1024; i++; }
|
||
return `${v.toFixed(i ? 1 : 0)} ${u[i]}`;
|
||
}
|
||
function fmtDate(s) { try { return new Intl.DateTimeFormat('de-DE',{dateStyle:'short',timeStyle:'medium'}).format(new Date(s)); } catch (_) { return s; } }
|
||
function psQuote(v) { return `'${String(v ?? '').replaceAll("'", "''")}'`; }
|
||
function downloadBlob(name, bytes, type='application/octet-stream') { const blob = bytes instanceof Blob ? bytes : new Blob([bytes],{type}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=name; a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); }
|
||
function kvRender(target, obj, labels={}) { const el=typeof target==='string'?$(target):target; el.replaceChildren(); for (const [k,v] of Object.entries(obj)) { if(v===undefined||v===null||v==='') continue; const row=document.createElement('div');row.className='kv-row';const key=document.createElement('div');key.className='kv-key';key.textContent=labels[k]||k;const val=document.createElement('div');val.className='kv-value';val.textContent=Array.isArray(v)?v.join(', '):String(v);row.append(key,val);el.append(row); } }
|
||
|
||
// -------------------- navigation and launcher --------------------
|
||
function openView(name) {
|
||
$$('.view').forEach(v => v.classList.toggle('active', v.id === `view-${name}`));
|
||
$$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === name));
|
||
if (name === 'clipboard') { loadClipRooms(); loadClipHistory(); startClipStream(); }
|
||
if (name === 'files') loadFiles();
|
||
}
|
||
$$('.nav-btn').forEach(b => b.addEventListener('click', () => openView(b.dataset.view)));
|
||
$$('[data-open-view]').forEach(b => b.addEventListener('click', () => openView(b.dataset.openView)));
|
||
|
||
async function loadStatus() {
|
||
try { const s = await api('/api/status'); $('#statusText').textContent = `${s.rooms.length} Räume · ${s.files} Dateien · 16 Werkzeuge`; $('#uploadStatus').textContent = `Maximale Dateigröße: ${fmtBytes(s.max_file_bytes)}.`; }
|
||
catch (_) { $('#statusText').textContent = 'Status nicht verfügbar'; }
|
||
}
|
||
async function loadApps() {
|
||
try { apps = await api('/api/apps'); const cats = [...new Set(apps.map(a => a.category).filter(Boolean))].sort(); cats.forEach(c => { const o=document.createElement('option');o.value=c;o.textContent=c;$('#appCategory').append(o); }); renderApps(); }
|
||
catch (_) { $('#appsGrid').innerHTML = `<div class="empty">Apps konnten nicht geladen werden.</div>`; }
|
||
}
|
||
function renderApps() {
|
||
const q = ($('#appSearch').value || '').toLocaleLowerCase('de'); const cat = $('#appCategory').value; const grid=$('#appsGrid'); grid.replaceChildren();
|
||
apps.filter(a => (!q || (a.title||'').toLocaleLowerCase('de').includes(q)) && (!cat || a.category===cat)).forEach(a => {
|
||
const link=document.createElement('a'); link.className='app-tile'; link.href=a.url; link.target='_blank'; link.rel='noopener noreferrer'; link.style.setProperty('--tile-color',a.color||'#427bbd');
|
||
const icon=document.createElement('div');icon.className='app-icon';icon.textContent=a.icon||'↗';
|
||
const title=document.createElement('div');title.className='app-title';title.textContent=a.title;
|
||
const badge=document.createElement('div');badge.className='app-cat';badge.textContent=a.category||'Anwendung';
|
||
link.append(icon,title,badge);grid.append(link);
|
||
});
|
||
if (!grid.children.length) { const e=document.createElement('div'); e.className='empty'; e.textContent='Keine passenden Anwendungen.'; grid.append(e); }
|
||
}
|
||
$('#appSearch').addEventListener('input',renderApps); $('#appCategory').addEventListener('change',renderApps);
|
||
|
||
// -------------------- clipboard --------------------
|
||
function room() { return ($('#clipRoom').value || 'default').trim(); }
|
||
|
||
async function loadClipRooms() {
|
||
const list=$('#clipRooms');
|
||
try {
|
||
const rooms=await api('/api/rooms/details');
|
||
list.replaceChildren();
|
||
rooms.forEach(info=>list.append(renderRoom(info)));
|
||
if(!rooms.length){const e=document.createElement('div');e.className='empty';e.textContent='Noch keine Räume vorhanden. Über „Raum öffnen / anlegen“ kannst du den ersten Raum erstellen.';list.append(e);}
|
||
return rooms;
|
||
} catch (_) {
|
||
list.innerHTML='<div class="empty">Raumliste konnte nicht geladen werden.</div>';
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function renderRoom(info) {
|
||
const row=document.createElement('div'); row.className='room-item';
|
||
if(info.name===room()) row.classList.add('active');
|
||
const open=document.createElement('button'); open.className='room-open'; open.type='button';
|
||
const name=document.createElement('span'); name.className='room-name'; name.textContent=info.name;
|
||
const meta=document.createElement('span'); meta.className='room-meta';
|
||
const parts=[`${info.count} Einträge`]; if(info.secrets) parts.push(`${info.secrets} Secrets`); if(info.last_active) parts.push(`zuletzt ${fmtDate(info.last_active)}`); meta.textContent=parts.join(' · ');
|
||
open.append(name,meta); open.addEventListener('click',()=>openClipRoom(info.name));
|
||
const actions=document.createElement('div'); actions.className='room-actions';
|
||
const clear=document.createElement('button'); clear.className='btn small'; clear.type='button'; clear.textContent='Leeren';
|
||
clear.addEventListener('click',async(e)=>{e.stopPropagation();await clearClipRoom(info.name);});
|
||
const del=document.createElement('button'); del.className='btn small danger'; del.type='button'; del.textContent='Löschen';
|
||
del.addEventListener('click',async(e)=>{e.stopPropagation();await deleteClipRoom(info.name);});
|
||
actions.append(clear,del); row.append(open,actions); return row;
|
||
}
|
||
|
||
async function openClipRoom(name, create=false) {
|
||
name=String(name||'').trim(); if(!name) return toast('Raumname fehlt');
|
||
try {
|
||
if(create) await api('/api/rooms',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});
|
||
$('#clipRoom').value=name; $('#clipRoomBadge').textContent=name;
|
||
await loadClipHistory(); startClipStream(); await loadClipRooms(); loadStatus();
|
||
} catch(e) { toast(e.message); }
|
||
}
|
||
|
||
async function clearClipRoom(name) {
|
||
if(!confirm(`Verlauf von „${name}“ wirklich leeren? Der Raum bleibt erhalten.`)) return;
|
||
try {
|
||
await api(`/api/${encodeURIComponent(name)}/history`,{method:'DELETE'});
|
||
if(name===room()) await loadClipHistory();
|
||
await loadClipRooms(); loadStatus(); toast(`Raum „${name}“ geleert`);
|
||
} catch(e) { toast(e.message); }
|
||
}
|
||
|
||
async function deleteClipRoom(name) {
|
||
if(!confirm(`Raum „${name}“ inklusive aller Einträge wirklich dauerhaft löschen?`)) return;
|
||
try {
|
||
await api(`/api/${encodeURIComponent(name)}`,{method:'DELETE'});
|
||
if(name===room()) {
|
||
if(clipStream){clipStream.close();clipStream=null;}
|
||
const remaining=await api('/api/rooms/details');
|
||
const next=remaining[0]?.name || 'default'; $('#clipRoom').value=next; $('#clipRoomBadge').textContent=next; await loadClipHistory(); if(remaining.length) startClipStream();
|
||
}
|
||
await loadClipRooms(); loadStatus(); toast(`Raum „${name}“ gelöscht`);
|
||
} catch(e) { toast(e.message); }
|
||
}
|
||
|
||
async function loadClipHistory() {
|
||
const r=room(); $('#clipRoomBadge').textContent=r;
|
||
try { const items=await api(`/api/${encodeURIComponent(r)}/history?limit=100`); const list=$('#clipHistory'); list.replaceChildren(); [...items].reverse().forEach(c=>list.append(renderClip(c))); if(!items.length){const e=document.createElement('div');e.className='empty';e.textContent='Noch keine Einträge in diesem Raum.';list.append(e);} }
|
||
catch(_){ $('#clipHistory').innerHTML='<div class="empty">Verlauf konnte nicht geladen werden.</div>'; }
|
||
}
|
||
function renderClip(c) {
|
||
const item=document.createElement('div');item.className='clip-item';
|
||
const top=document.createElement('div');top.className='clip-top'; const left=document.createElement('span');left.textContent=`${c.type||'text'}${c.author?' · '+c.author:''}`; const right=document.createElement('span');right.textContent=fmtDate(c.created_at);top.append(left,right);
|
||
const content=document.createElement('div');content.className='clip-content'; content.textContent=c.secret?'••••••••••••':(c.content||''); if(c.secret)content.classList.add('clip-secret');
|
||
const actions=document.createElement('div');actions.className='clip-actions';
|
||
if(c.secret){const reveal=document.createElement('button');reveal.className='btn small';reveal.textContent=c.one_time?'Einmalig abrufen':'Secret abrufen';reveal.addEventListener('click',async()=>{try{const full=await api(`/api/${encodeURIComponent(c.room)}/clip/${encodeURIComponent(c.id)}`);content.textContent=full.content;content.classList.remove('clip-secret');await navigator.clipboard.writeText(full.content).catch(()=>{});toast('Secret abgerufen und lokal kopiert');if(full.one_time){setTimeout(loadClipHistory,300);setTimeout(loadClipRooms,300);}}catch(e){toast(e.message)}});actions.append(reveal);} else {const copy=document.createElement('button');copy.className='btn small';copy.textContent='Kopieren';copy.addEventListener('click',async()=>{await navigator.clipboard.writeText(c.content||'');toast('Lokal kopiert');});actions.append(copy);}
|
||
if(c.expires_at){const exp=document.createElement('span');exp.className='badge';exp.textContent=`bis ${fmtDate(c.expires_at)}`;actions.append(exp);} item.append(top,content,actions);return item;
|
||
}
|
||
function startClipStream(){ if(clipStream)clipStream.close(); clipStream=null; try{clipStream=new EventSource(`/api/${encodeURIComponent(room())}/stream`);clipStream.addEventListener('clip',()=>{loadClipHistory();loadClipRooms();});}catch(_){} }
|
||
$('#clipRoom').addEventListener('change',()=>openClipRoom(room(),true));
|
||
$('#clipOpenRoom').addEventListener('click',()=>openClipRoom(room(),true));
|
||
$('#clipRoomsRefresh').addEventListener('click',loadClipRooms);
|
||
$('#clipRefresh').addEventListener('click',()=>{loadClipHistory();loadClipRooms();});
|
||
$('#clipSubmit').addEventListener('click',async()=>{const r=room();const body={type:'text',content:$('#clipContent').value,author:$('#clipAuthor').value,secret:$('#clipSecret').checked,one_time:$('#clipOneTime').checked,ttl_minutes:Number($('#clipTTL').value||0)};try{await api(`/api/${encodeURIComponent(r)}/clip`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});$('#clipContent').value='';toast('Eintrag abgelegt');await loadClipHistory();await loadClipRooms();startClipStream();loadStatus();}catch(e){toast(e.message)}});
|
||
$('#clipPasteLocal').addEventListener('click',async()=>{try{$('#clipContent').value=await navigator.clipboard.readText();}catch(_){toast('Browser-Zwischenablage konnte nicht gelesen werden')}});
|
||
$('#clipClear').addEventListener('click',()=>clearClipRoom(room()));
|
||
|
||
// -------------------- password --------------------
|
||
$('#pwGenerate').addEventListener('click',async()=>{const btn=$('#pwGenerate');btn.disabled=true;try{const count=Math.max(1,Math.min(20,Number($('#pwCount').value||1)));const params=new URLSearchParams({count:String(count)});if($('#pwStore').checked){params.set('room',($('#pwRoom').value||'default').trim());params.set('ttl_minutes',String(Number($('#pwTTL').value||0)));params.set('one_time',String($('#pwOneTime').checked));params.set('secret','true');}const data=await api(`/api/generate?${params}`,{method:'POST'});$('#pwOutput').textContent=data.map(x=>x.password).join('\n');const meta=$('#pwMeta');meta.replaceChildren();const ent=document.createElement('span');ent.className='meta-pill';ent.textContent=`Entropy ≈ ${data[0]?.entropy_bits?.toFixed(1)||'–'} bit`;meta.append(ent);if(data.some(x=>x.stored)){const p=document.createElement('span');p.className='meta-pill';p.textContent=`Im Raum ${$('#pwRoom').value||'default'} abgelegt`;meta.append(p);}toast('Passwort erzeugt');loadStatus();}catch(e){$('#pwOutput').textContent=`Fehler: ${e.message}`;}finally{btn.disabled=false;}});
|
||
$('#pwCopy').addEventListener('click',async()=>{try{await navigator.clipboard.writeText($('#pwOutput').textContent);toast('Lokal kopiert');}catch(_){toast('Kopieren fehlgeschlagen')}});
|
||
|
||
// -------------------- file transfer --------------------
|
||
async function loadFiles(){try{const files=await api('/api/files');const list=$('#filesList');list.replaceChildren();files.forEach(f=>list.append(renderFile(f)));if(!files.length){const e=document.createElement('div');e.className='empty';e.textContent='Noch keine Dateien vorhanden.';list.append(e);}loadStatus();}catch(_){$('#filesList').innerHTML='<div class="empty">Dateiliste konnte nicht geladen werden.</div>';}}
|
||
function renderFile(f){const row=document.createElement('div');row.className='file-row';const a=document.createElement('div');const n=document.createElement('div');n.className='file-name';n.textContent=f.name;const sub=document.createElement('div');sub.className='file-sub';sub.textContent=`${fmtBytes(f.size)} · ${fmtDate(f.uploaded_at)}${f.uploader?' · '+f.uploader:''}`;a.append(n,sub);const b=document.createElement('div');const h=document.createElement('div');h.className='file-hash';h.textContent=`SHA-256 ${f.sha256}`;b.append(h);const actions=document.createElement('div');actions.className='file-actions';const dl=document.createElement('a');dl.className='btn small';dl.href=`/api/files/${encodeURIComponent(f.id)}`;dl.textContent='Download';const del=document.createElement('button');del.className='btn small danger';del.textContent='Löschen';del.addEventListener('click',async()=>{if(!confirm(`„${f.name}“ wirklich löschen?`))return;try{await api(`/api/files/${encodeURIComponent(f.id)}`,{method:'DELETE'});toast('Datei gelöscht');loadFiles();}catch(e){toast(e.message)}});actions.append(dl,del);row.append(a,b,actions);return row;}
|
||
$('#uploadFile').addEventListener('change',()=>{$('#filePicked').textContent=$('#uploadFile').files[0]?.name||'Keine Datei ausgewählt';});
|
||
$('#uploadForm').addEventListener('submit',async(e)=>{e.preventDefault();const file=$('#uploadFile').files[0];if(!file)return;const form=new FormData();form.append('file',file);form.append('uploader',$('#fileUploader').value);const btn=e.currentTarget.querySelector('button[type=submit]');btn.disabled=true;$('#uploadStatus').textContent='Upload läuft…';try{const m=await api('/api/files',{method:'POST',body:form});$('#uploadStatus').textContent=`Hochgeladen: ${m.name} · SHA-256 ${m.sha256}`;$('#uploadFile').value='';$('#filePicked').textContent='Keine Datei ausgewählt';toast('Datei hochgeladen');loadFiles();}catch(err){$('#uploadStatus').textContent=`Fehler: ${err.message}`;}finally{btn.disabled=false;}});
|
||
$('#filesRefresh').addEventListener('click',loadFiles);
|
||
|
||
// -------------------- tools navigation --------------------
|
||
function openTool(name) { $$('.tool-panel').forEach(p=>p.classList.toggle('active',p.id===`tool-${name}`)); $$('.tool-nav-btn').forEach(b=>b.classList.toggle('active',b.dataset.tool===name)); }
|
||
$$('.tool-nav-btn').forEach(b=>b.addEventListener('click',()=>openTool(b.dataset.tool)));
|
||
$('#toolSearch').addEventListener('input',()=>{const q=$('#toolSearch').value.toLocaleLowerCase('de').trim(); const buttons=$$('.tool-nav-btn'); buttons.forEach(b=>b.hidden=!!q&&!`${b.textContent} ${b.dataset.keywords||''}`.toLocaleLowerCase('de').includes(q)); const active=buttons.find(b=>b.classList.contains('active')); if(active?.hidden){const first=buttons.find(b=>!b.hidden);if(first)openTool(first.dataset.tool);}});
|
||
|
||
// -------------------- onboarding assistant --------------------
|
||
function asciiName(v) { return String(v||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/ß/g,'ss').replace(/[^A-Za-z0-9.-]/g,'').toLowerCase(); }
|
||
function buildOnboarding() {
|
||
const first=$('#onbFirst').value.trim(), last=$('#onbLast').value.trim(); if(!first||!last) throw new Error('Vorname und Nachname fehlen');
|
||
const sam=(asciiName(first).slice(0,1)+asciiName(last)).slice(0,20); const domain=$('#onbDomain').value.trim(); const mailDomain=($('#onbMailDomain').value.trim()||domain); const display=$('#onbDisplayMode').value==='lastfirst'?`${last}, ${first}`:`${first} ${last}`; const upn=domain?`${sam}@${domain}`:sam; const mail=mailDomain?`${asciiName(first)}.${asciiName(last)}@${mailDomain}`:'';
|
||
$('#onbSam').textContent=sam;$('#onbUPN').textContent=upn;$('#onbMail').textContent=mail||'–';$('#onbDisplay').textContent=display;
|
||
const args=[`-Name ${psQuote(display)}`,`-GivenName ${psQuote(first)}`,`-Surname ${psQuote(last)}`,`-DisplayName ${psQuote(display)}`,`-SamAccountName ${psQuote(sam)}`,`-UserPrincipalName ${psQuote(upn)}`];
|
||
const optional=[['#onbOU','-Path'],['#onbDepartment','-Department'],['#onbTitle','-Title'],['#onbOffice','-Office']]; optional.forEach(([id,arg])=>{const v=$(id).value.trim();if(v)args.push(`${arg} ${psQuote(v)}`);}); if(mail)args.push(`-EmailAddress ${psQuote(mail)}`);
|
||
args.push('-AccountPassword $InitialPassword','-Enabled $true','-ChangePasswordAtLogon $true');
|
||
const bt=String.fromCharCode(96);
|
||
let ps="# Kennwort separat aus dem PAW-Secret-Transfer übernehmen\n$InitialPassword = Read-Host 'Initiales Kennwort' -AsSecureString\n\nNew-ADUser "+bt+"\n "+args.join(" "+bt+"\n ")+"\n";
|
||
const groups=$('#onbGroups').value.split(',').map(x=>x.trim()).filter(Boolean); if(groups.length){ps+='\n# Gruppenmitgliedschaften\n'+groups.map(g=>`Add-ADGroupMember -Identity ${psQuote(g)} -Members ${psQuote(sam)}`).join('\n')+'\n';}
|
||
$('#onbPS').value=ps; return {sam,upn,mail,display};
|
||
}
|
||
$('#onbBuild').addEventListener('click',()=>{try{buildOnboarding();toast('Onboarding-Daten erzeugt')}catch(e){toast(e.message)}});
|
||
$('#onbCopy').addEventListener('click',async()=>{try{if(!$('#onbPS').value)buildOnboarding();await navigator.clipboard.writeText($('#onbPS').value);toast('PowerShell kopiert')}catch(e){toast(e.message)}});
|
||
$('#onbPassword').addEventListener('click',async()=>{try{const data=buildOnboarding();const params=new URLSearchParams({count:'1',room:($('#onbRoom').value||'onboarding').trim(),ttl_minutes:String(Number($('#onbTTL').value||15)),one_time:'true',secret:'true'});const r=await api(`/api/generate?${params}`,{method:'POST'});$('#onbSecretStatus').textContent=`Kennwort für ${data.sam} als einmaliges Secret abgelegt · Raum ${$('#onbRoom').value||'onboarding'} · Clip ${r[0].clip_id}.`;toast('Kennwort erzeugt und als Secret abgelegt');loadStatus();}catch(e){toast(e.message)}});
|
||
|
||
// -------------------- file/hash inspector --------------------
|
||
$('#fiFile').addEventListener('change',()=>$('#fiPicked').textContent=$('#fiFile').files[0]?.name||'Keine Datei ausgewählt');
|
||
$('#fiForm').addEventListener('submit',async e=>{e.preventDefault();const f=$('#fiFile').files[0];if(!f)return;const fd=new FormData();fd.append('file',f);try{const x=await api('/api/tools/file-inspect',{method:'POST',body:fd});lastFileSHA256=x.sha256;kvRender('#fiResult',x,{name:'Dateiname',size:'Größe (Bytes)',mime:'Erkannter MIME-Type',extension:'Erweiterung',magic_hex:'Erste Bytes (Hex)',md5:'MD5',sha256:'SHA-256',sha512:'SHA-512'});}catch(err){toast(err.message)}});
|
||
$('#fiCopyHash').addEventListener('click',async()=>{if(!lastFileSHA256)return toast('Zuerst Datei analysieren');await navigator.clipboard.writeText(lastFileSHA256);toast('SHA-256 kopiert')});
|
||
|
||
// -------------------- certificate inspector --------------------
|
||
$('#certFile').addEventListener('change',()=>$('#certPicked').textContent=$('#certFile').files[0]?.name||'Keine Datei ausgewählt');
|
||
$('#certForm').addEventListener('submit',async e=>{e.preventDefault();const f=$('#certFile').files[0];if(!f)return;const fd=new FormData();fd.append('file',f);fd.append('password',$('#certPassword').value);const out=$('#certResult');out.textContent='Analyse läuft…';try{const x=await api('/api/tools/cert-inspect',{method:'POST',body:fd});out.replaceChildren();x.certificates.forEach((c,i)=>{const card=document.createElement('section');card.className='cert-card';const h=document.createElement('h3');h.textContent=`Zertifikat ${i+1}${c.expired?' · ABGELAUFEN':''}`; if(c.expired)h.classList.add('status-bad');card.append(h);const kv=document.createElement('div');kv.className='kv-list';kvRender(kv,{subject:c.subject,issuer:c.issuer,serial:c.serial,dns_names:c.dns_names,ip_addresses:c.ip_addresses,emails:c.emails,not_before:fmtDate(c.not_before),not_after:fmtDate(c.not_after),days_left:c.days_left,is_ca:c.is_ca,signature_algorithm:c.signature_algorithm,public_key_algorithm:c.public_key_algorithm,key_usage:c.key_usage,ext_key_usage:c.ext_key_usage,sha1_thumbprint:c.sha1_thumbprint,sha256_thumbprint:c.sha256_thumbprint},{subject:'Subject',issuer:'Issuer',serial:'Seriennummer',dns_names:'DNS SANs',ip_addresses:'IP SANs',emails:'E-Mail SANs',not_before:'Gültig ab',not_after:'Gültig bis',days_left:'Tage verbleibend',is_ca:'CA-Zertifikat',signature_algorithm:'Signaturalgorithmus',public_key_algorithm:'Public-Key-Algorithmus',key_usage:'Key Usage',ext_key_usage:'Extended Key Usage',sha1_thumbprint:'SHA-1 Thumbprint',sha256_thumbprint:'SHA-256 Thumbprint'});card.append(kv);out.append(card);});}catch(err){out.textContent='';toast(err.message)}});
|
||
|
||
// -------------------- formatter --------------------
|
||
function formatXML(xml, compact=false) { const parser=new DOMParser();const doc=parser.parseFromString(xml,'application/xml');const err=doc.querySelector('parsererror');if(err)throw new Error(err.textContent.split('\n')[0]||'Ungültiges XML');let s=new XMLSerializer().serializeToString(doc);if(compact)return s;s=s.replace(/>\s*</g,'><').replace(/(>)(<)(\/?)/g,'$1\n$2$3');let depth=0;return s.split('\n').map(line=>{const t=line.trim();if(/^<\//.test(t))depth=Math.max(0,depth-1);const out=' '.repeat(depth)+t;if(/^<[^!?/][^>]*[^/]?>$/.test(t)&&!/<\/[^>]+>$/.test(t))depth++;return out;}).join('\n'); }
|
||
function basicYAML(text, action) { const lines=text.replace(/\r\n/g,'\n').split('\n');for(let i=0;i<lines.length;i++){if(/^\t+/.test(lines[i])||/^ +\t/.test(lines[i]))throw new Error(`YAML: Tab-Einrückung in Zeile ${i+1}`);const raw=lines[i].trim();if(!raw||raw.startsWith('#'))continue;if(/^[-?:]\s*$/.test(raw))continue;const quotes=(raw.match(/"/g)||[]).length;if(quotes%2)throw new Error(`YAML: ungerade Anzahl doppelter Anführungszeichen in Zeile ${i+1}`);}if(action==='validate')return 'Basisprüfung erfolgreich. Hinweis: Dies ist keine vollständige YAML-Schema-/Semantikprüfung.';if(action==='minify')return lines.map(x=>x.trimEnd()).filter(x=>x.trim()!=='').join('\n');return lines.map(x=>x.replace(/[ \t]+$/,'')).join('\n').trim()+"\n"; }
|
||
$('#fmtRun').addEventListener('click',()=>{try{const type=$('#fmtType').value,act=$('#fmtAction').value,input=$('#fmtInput').value;let out='';if(type==='JSON'){const o=JSON.parse(input);out=act==='validate'?'JSON ist syntaktisch gültig.':JSON.stringify(o,null,act==='pretty'?2:0);}else if(type==='XML'){out=act==='validate'?(formatXML(input,true),'XML ist syntaktisch gültig.'):formatXML(input,act==='minify');}else out=basicYAML(input,act);$('#fmtOutput').value=out;$('#fmtStatus').textContent=type==='YAML'?'YAML-Basisprüfung/Normalisierung lokal abgeschlossen.':'Lokale Verarbeitung erfolgreich.';}catch(e){$('#fmtOutput').value='';$('#fmtStatus').textContent=`Fehler: ${e.message}`;}});
|
||
$('#fmtCopy').addEventListener('click',async()=>{await navigator.clipboard.writeText($('#fmtOutput').value);toast('Ergebnis kopiert')});
|
||
|
||
// -------------------- text diff --------------------
|
||
function lineDiff(a,b){const A=a.replace(/\r\n/g,'\n').split('\n'),B=b.replace(/\r\n/g,'\n').split('\n');if(A.length>500||B.length>500)throw new Error('Diff ist auf 500 Zeilen je Seite begrenzt');const dp=Array.from({length:A.length+1},()=>new Uint16Array(B.length+1));for(let i=A.length-1;i>=0;i--)for(let j=B.length-1;j>=0;j--)dp[i][j]=A[i]===B[j]?dp[i+1][j+1]+1:Math.max(dp[i+1][j],dp[i][j+1]);let i=0,j=0,out=[];while(i<A.length||j<B.length){if(i<A.length&&j<B.length&&A[i]===B[j]){out.push([' ',A[i]]);i++;j++;}else if(j<B.length&&(i===A.length||dp[i][j+1]>=dp[i+1][j])){out.push(['+',B[j++]]);}else{out.push(['-',A[i++]]);}}return out;}
|
||
$('#diffRun').addEventListener('click',()=>{const out=$('#diffOutput');out.replaceChildren();try{for(const [t,line] of lineDiff($('#diffA').value,$('#diffB').value)){const s=document.createElement('span');s.className=t==='+'?'diff-add':t==='-'?'diff-del':'diff-same';s.textContent=`${t} ${line}\n`;out.append(s);}}catch(e){out.textContent=`Fehler: ${e.message}`;}});
|
||
|
||
// -------------------- AD converters --------------------
|
||
function newUUID(){const b=crypto.getRandomValues(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;const h=[...b].map(x=>x.toString(16).padStart(2,'0'));return `${h.slice(0,4).join('')}-${h.slice(4,6).join('')}-${h.slice(6,8).join('')}-${h.slice(8,10).join('')}-${h.slice(10).join('')}`;}
|
||
function guidToAD(g){const raw=g.replace(/[{}-]/g,'').toLowerCase();if(!/^[0-9a-f]{32}$/.test(raw))throw new Error('GUID/Hex muss 16 Bytes enthalten');const b=raw.match(/../g);const ad=[...b.slice(0,4).reverse(),...b.slice(4,6).reverse(),...b.slice(6,8).reverse(),...b.slice(8)];return {hex:ad.join(''),ldap:ad.map(x=>'\\'+x).join('')};}
|
||
function adHexToGuid(h){const raw=h.replace(/[^0-9a-f]/gi,'').toLowerCase();if(raw.length!==32)throw new Error('AD-Hex muss 16 Bytes enthalten');const b=raw.match(/../g);const n=[...b.slice(0,4).reverse(),...b.slice(4,6).reverse(),...b.slice(6,8).reverse(),...b.slice(8)];return `${n.slice(0,4).join('')}-${n.slice(4,6).join('')}-${n.slice(6,8).join('')}-${n.slice(8,10).join('')}-${n.slice(10).join('')}`;}
|
||
$('#guidNew').addEventListener('click',()=>{$('#guidInput').value=newUUID();$('#guidOutput').textContent=''});
|
||
$('#guidConvert').addEventListener('click',()=>{try{const v=$('#guidInput').value.trim();if(v.includes('-')){const x=guidToAD(v);$('#guidOutput').textContent=`AD objectGUID Hex:\n${x.hex}\n\nLDAP escaped:\n${x.ldap}`;}else{$('#guidOutput').textContent=`GUID aus AD-Bytefolge:\n${adHexToGuid(v)}`;}}catch(e){$('#guidOutput').textContent=`Fehler: ${e.message}`;}});
|
||
function sidStringToHex(s){const p=s.trim().split('-');if(p[0]!=='S'||p.length<4)throw new Error('Ungültige SID');const rev=Number(p[1]),auth=BigInt(p[2]),subs=p.slice(3).map(BigInt);if(subs.length>255)throw new Error('Zu viele Subauthorities');const out=[rev,subs.length];for(let i=5;i>=0;i--)out.push(Number((auth>>BigInt(i*8))&255n));for(const x of subs)for(let i=0;i<4;i++)out.push(Number((x>>BigInt(i*8))&255n));return out.map(x=>x.toString(16).padStart(2,'0')).join('');}
|
||
function sidHexToString(h){const raw=h.replace(/[^0-9a-f]/gi,'');if(raw.length<16||raw.length%2)throw new Error('Ungültiges SID-Hex');const b=raw.match(/../g).map(x=>parseInt(x,16));const rev=b[0],cnt=b[1];if(b.length<8+cnt*4)throw new Error('SID-Hex zu kurz');let auth=0n;for(let i=2;i<8;i++)auth=(auth<<8n)|BigInt(b[i]);const subs=[];let o=8;for(let n=0;n<cnt;n++){let x=0n;for(let i=0;i<4;i++)x|=BigInt(b[o++])<<BigInt(i*8);subs.push(x);}return `S-${rev}-${auth}-${subs.join('-')}`;}
|
||
$('#sidConvert').addEventListener('click',()=>{try{const v=$('#sidInput').value.trim();$('#sidOutput').textContent=v.toUpperCase().startsWith('S-')?`Binär/Hex (little endian SubAuthorities):\n${sidStringToHex(v)}`:`SID:\n${sidHexToString(v)}`;}catch(e){$('#sidOutput').textContent=`Fehler: ${e.message}`;}});
|
||
function parseTimeInput(v){v=v.trim();const gen=v.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.\d+)?Z$/);if(gen)return new Date(Date.UTC(+gen[1],+gen[2]-1,+gen[3],+gen[4],+gen[5],+gen[6]));if(/^\d+$/.test(v)){const n=BigInt(v);if(n>1000000000000000n){const ms=Number(n/10000n-11644473600000n);return new Date(ms);}if(n>100000000000n)return new Date(Number(n));return new Date(Number(n)*1000);}const d=new Date(v);if(Number.isNaN(d.getTime()))throw new Error('Zeitwert nicht erkannt');return d;}
|
||
function formatGeneralized(d){const p=n=>String(n).padStart(2,'0');return `${d.getUTCFullYear()}${p(d.getUTCMonth()+1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}.0Z`;}
|
||
function convertTime(){try{const d=parseTimeInput($('#timeInput').value);const ms=BigInt(d.getTime());const ft=(ms+11644473600000n)*10000n;$('#timeOutput').textContent=`ISO 8601: ${d.toISOString()}\nLokal: ${d.toLocaleString('de-DE')}\nUnix Sekunden: ${Math.floor(d.getTime()/1000)}\nUnix Millisekunden: ${d.getTime()}\nWindows FILETIME / AD: ${ft}\nLDAP GeneralizedTime: ${formatGeneralized(d)}`;}catch(e){$('#timeOutput').textContent=`Fehler: ${e.message}`;}}
|
||
$('#timeNow').addEventListener('click',()=>{$('#timeInput').value=new Date().toISOString();convertTime();});$('#timeConvert').addEventListener('click',convertTime);
|
||
|
||
// -------------------- DNS / subnet / connectivity --------------------
|
||
$('#dnsRun').addEventListener('click',async()=>{const out=$('#dnsOutput');out.textContent='Abfrage läuft…';try{const x=await api('/api/tools/dns',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:$('#dnsName').value,type:$('#dnsType').value})});out.textContent=`${x.type} ${x.name} · ${x.duration_ms} ms\n\n${x.results.length?x.results.join('\n'):'Keine Ergebnisse'}`;}catch(e){out.textContent=`Fehler: ${e.message}`;}});
|
||
$('#subnetRun').addEventListener('click',async()=>{try{const x=await api('/api/tools/subnet',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cidr:$('#subnetInput').value})});kvRender('#subnetOutput',x,{input:'Eingabe',network:'Netz',prefix_length:'Präfix',address_bits:'Adressbits',family:'Familie',netmask:'Netzmaske',broadcast:'Broadcast',first_address:'Erste Adresse',last_address:'Letzte Adresse',first_host:'Erster Host',last_host:'Letzter Host',address_count:'Adressen gesamt',usable_hosts:'Nutzbare Hosts'});}catch(e){toast(e.message)}});
|
||
$('#connRun').addEventListener('click',async()=>{try{const x=await api('/api/tools/connectivity',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:$('#connHost').value,port:Number($('#connPort').value),timeout_ms:Number($('#connTimeout').value)})});kvRender('#connOutput',x,{host:'Host',port:'Port',resolved_ips:'DNS-Auflösung',latency_ms:'TCP-Latenz ms',reachable:'Erreichbar',remote:'Remote Endpoint',error:'Fehler'});const first=$('#connOutput .kv-row:nth-child(5) .kv-value');if(first)first.classList.add(x.reachable?'status-ok':'status-bad');}catch(e){toast(e.message)}});
|
||
|
||
// -------------------- encoder --------------------
|
||
function bytesToBase64(bytes){let s='';const step=0x8000;for(let i=0;i<bytes.length;i+=step)s+=String.fromCharCode(...bytes.subarray(i,i+step));return btoa(s);}function base64ToBytes(s){const bin=atob(s.replace(/\s+/g,''));return Uint8Array.from(bin,c=>c.charCodeAt(0));}
|
||
function encoderRun(){try{const type=$('#encType').value,mode=$('#encMode').value,input=$('#encInput').value;let out='';if(type==='url')out=mode==='encode'?encodeURIComponent(input):decodeURIComponent(input);else if(type==='base64')out=mode==='encode'?bytesToBase64(new TextEncoder().encode(input)):new TextDecoder().decode(base64ToBytes(input));else if(mode==='encode')out=[...new TextEncoder().encode(input)].map(b=>b.toString(16).padStart(2,'0')).join('');else{const h=input.replace(/\s+/g,'');if(!/^[0-9a-f]*$/i.test(h)||h.length%2)throw new Error('Ungültiges Hex');out=new TextDecoder().decode(Uint8Array.from(h.match(/../g)||[],x=>parseInt(x,16)));}$('#encOutput').value=out;}catch(e){$('#encOutput').value=`Fehler: ${e.message}`;}}
|
||
$('#encRun').addEventListener('click',encoderRun);$('#encSwap').addEventListener('click',()=>{const x=$('#encInput').value;$('#encInput').value=$('#encOutput').value;$('#encOutput').value=x;$('#encMode').value=$('#encMode').value==='encode'?'decode':'encode';});
|
||
|
||
// -------------------- PowerShell builder --------------------
|
||
function psBuild(){
|
||
const t=$('#psTemplate').value,id=$('#psIdentity').value.trim(),g=$('#psGroup').value.trim(),f=$('#psFirst').value.trim(),l=$('#psLast').value.trim(),ou=$('#psOU').value.trim();
|
||
let s='';
|
||
switch(t){
|
||
case 'getuser': s=`Get-ADUser -Identity ${psQuote(id)} -Properties *`; break;
|
||
case 'unlock': s=`Unlock-ADAccount -Identity ${psQuote(id)}`; break;
|
||
case 'addgroup': s=`Add-ADGroupMember -Identity ${psQuote(g)} -Members ${psQuote(id)}`; break;
|
||
case 'resetpw': s=`$NewPassword = Read-Host 'Neues Kennwort' -AsSecureString\nSet-ADAccountPassword -Identity ${psQuote(id)} -Reset -NewPassword $NewPassword\nSet-ADUser -Identity ${psQuote(id)} -ChangePasswordAtLogon $true`; break;
|
||
case 'newuser': {
|
||
const display=`${l}, ${f}`.replace(/^, |, $/,'');
|
||
const args=[`-Name ${psQuote(display)}`,`-GivenName ${psQuote(f)}`,`-Surname ${psQuote(l)}`];
|
||
if(id)args.push(`-SamAccountName ${psQuote(id)}`); if(ou)args.push(`-Path ${psQuote(ou)}`);
|
||
const bt=String.fromCharCode(96);
|
||
s="$InitialPassword = Read-Host 'Initiales Kennwort' -AsSecureString\nNew-ADUser "+bt+"\n "+args.join(" "+bt+"\n ")+" "+bt+"\n -AccountPassword $InitialPassword -Enabled $true -ChangePasswordAtLogon $true";
|
||
break;
|
||
}
|
||
}
|
||
$('#psOutput').textContent=s; return s;
|
||
}
|
||
$('#psBuild').addEventListener('click',()=>{psBuild();toast('Befehl erzeugt')});$('#psCopy').addEventListener('click',async()=>{await navigator.clipboard.writeText(psBuild());toast('PowerShell kopiert')});
|
||
|
||
// -------------------- CSV viewer --------------------
|
||
function detectDelimiter(text){const first=(text.split(/\r?\n/).find(x=>x.trim())||'');const cand=[';',',','\t'];return cand.map(d=>[d,(first.match(new RegExp(d==='\t'?'\\t':`\\${d}`,'g'))||[]).length]).sort((a,b)=>b[1]-a[1])[0][0];}
|
||
function parseCSV(text,delim){const rows=[];let row=[],field='',quoted=false;for(let i=0;i<text.length;i++){const c=text[i];if(quoted){if(c==='"'&&text[i+1]==='"'){field+='"';i++;}else if(c==='"')quoted=false;else field+=c;}else if(c==='"')quoted=true;else if(c===delim){row.push(field);field='';}else if(c==='\n'){row.push(field.replace(/\r$/,''));rows.push(row);row=[];field='';}else field+=c;}if(field||row.length){row.push(field.replace(/\r$/,''));rows.push(row);}return rows;}
|
||
function renderCSV(){const q=$('#csvFilter').value.toLocaleLowerCase('de');const wrap=$('#csvTableWrap');wrap.replaceChildren();if(!csvRows.length)return;const head=csvRows[0];const data=csvRows.slice(1).filter(r=>!q||r.some(c=>c.toLocaleLowerCase('de').includes(q)));const table=document.createElement('table');table.className='data-table';const tr=document.createElement('tr');head.forEach(h=>{const th=document.createElement('th');th.textContent=h;tr.append(th)});const thead=document.createElement('thead');thead.append(tr);table.append(thead);const body=document.createElement('tbody');data.slice(0,1000).forEach(r=>{const tr=document.createElement('tr');for(let i=0;i<head.length;i++){const td=document.createElement('td');td.textContent=r[i]??'';tr.append(td)}body.append(tr)});table.append(body);wrap.append(table);$('#csvMeta').textContent=`${data.length} von ${Math.max(0,csvRows.length-1)} Datenzeilen · ${head.length} Spalten${data.length>1000?' · Anzeige auf 1000 Zeilen begrenzt':''}`;}
|
||
$('#csvFile').addEventListener('change',async()=>{const f=$('#csvFile').files[0];$('#csvPicked').textContent=f?.name||'Keine Datei ausgewählt';if(!f)return;const text=await f.text();const sel=$('#csvDelimiter').value;csvCurrentDelimiter=sel==='auto'?detectDelimiter(text):sel;csvRows=parseCSV(text,csvCurrentDelimiter);renderCSV();});$('#csvFilter').addEventListener('input',renderCSV);$('#csvDelimiter').addEventListener('change',()=>{$('#csvFile').dispatchEvent(new Event('change'))});
|
||
function csvEscape(v,d){v=String(v??'');return /["\r\n]/.test(v)||v.includes(d)?`"${v.replaceAll('"','""')}"`:v;}
|
||
$('#csvExport').addEventListener('click',()=>{if(!csvRows.length)return toast('Keine CSV geladen');const q=$('#csvFilter').value.toLocaleLowerCase('de');const rows=[csvRows[0],...csvRows.slice(1).filter(r=>!q||r.some(c=>c.toLocaleLowerCase('de').includes(q)))];downloadBlob('filtered.csv','\uFEFF'+rows.map(r=>r.map(v=>csvEscape(v,csvCurrentDelimiter)).join(csvCurrentDelimiter)).join('\r\n'),'text/csv;charset=utf-8');});
|
||
|
||
// -------------------- regex --------------------
|
||
$('#regexRun').addEventListener('click',()=>{const out=$('#regexOutput');try{const flags=$('#regexFlags').value.replace(/[^dgimsuvy]/g,'');const global=flags.includes('g')?flags:flags+'g';const re=new RegExp($('#regexPattern').value,global);const text=$('#regexText').value;const matches=[...text.matchAll(re)];out.textContent=matches.length?matches.slice(0,200).map((m,i)=>`#${i+1} index=${m.index} ${JSON.stringify(m[0])}${m.length>1?' groups='+JSON.stringify(m.slice(1)):''}`).join('\n'):'Keine Treffer.';}catch(e){out.textContent=`Fehler: ${e.message}`;}});
|
||
|
||
// -------------------- text transformer --------------------
|
||
$('#transformRun').addEventListener('click',()=>{let lines=$('#transformInput').value.replace(/\r\n/g,'\n').split('\n');const a=$('#transformAction').value,x=$('#transformAffix').value;switch(a){case'trim':lines=lines.map(s=>s.trim());break;case'sort':lines.sort((a,b)=>a.localeCompare(b,'de',{numeric:true}));break;case'unique':lines=[...new Set(lines)];break;case'upper':lines=lines.map(s=>s.toLocaleUpperCase('de'));break;case'lower':lines=lines.map(s=>s.toLocaleLowerCase('de'));break;case'prefix':lines=lines.map(s=>x+s);break;case'suffix':lines=lines.map(s=>s+x);break;case'reverse':lines.reverse();break;}$('#transformOutput').value=lines.join('\n');});
|
||
|
||
// -------------------- ZIP archive viewer --------------------
|
||
$('#archiveFile').addEventListener('change',()=>$('#archivePicked').textContent=$('#archiveFile').files[0]?.name||'Keine Datei ausgewählt');
|
||
$('#archiveForm').addEventListener('submit',async e=>{e.preventDefault();const f=$('#archiveFile').files[0];if(!f)return;const fd=new FormData();fd.append('file',f);try{const x=await api('/api/tools/archive',{method:'POST',body:fd});$('#archiveMeta').textContent=`${x.entry_count} Einträge · komprimiert ${fmtBytes(x.compressed_bytes)} · entpackt ${fmtBytes(x.uncompressed_bytes)}`;const wrap=$('#archiveTable');wrap.replaceChildren();const table=document.createElement('table');table.className='data-table';const head=document.createElement('tr');['Name','Größe','Komprimiert','Typ','Geändert'].forEach(x=>{const th=document.createElement('th');th.textContent=x;head.append(th)});const thead=document.createElement('thead');thead.append(head);table.append(thead);const body=document.createElement('tbody');x.entries.forEach(en=>{const tr=document.createElement('tr');[en.name,fmtBytes(en.size),fmtBytes(en.compressed_size),en.directory?'Verzeichnis':`ZIP method ${en.method}`,fmtDate(en.modified)].forEach(v=>{const td=document.createElement('td');td.textContent=v;tr.append(td)});body.append(tr)});table.append(body);wrap.append(table);}catch(err){toast(err.message)}});
|
||
|
||
// -------------------- encoding converter --------------------
|
||
let encodingOriginalName='converted.txt';
|
||
$('#encodingFile').addEventListener('change',async()=>{const f=$('#encodingFile').files[0];$('#encodingPicked').textContent=f?.name||'Keine Datei ausgewählt';if(!f)return;encodingOriginalName=f.name;try{const b=await f.arrayBuffer();$('#encodingText').value=new TextDecoder($('#encodingSource').value).decode(b);}catch(e){toast(`Encoding konnte nicht gelesen werden: ${e.message}`)}});
|
||
$('#encodingSource').addEventListener('change',()=>{$('#encodingFile').dispatchEvent(new Event('change'))});
|
||
$('#encodingDownload').addEventListener('click',()=>{let bytes=new TextEncoder().encode($('#encodingText').value);if($('#encodingTarget').value==='utf8bom'){const out=new Uint8Array(bytes.length+3);out.set([0xef,0xbb,0xbf]);out.set(bytes,3);bytes=out;}downloadBlob(encodingOriginalName.replace(/(\.[^.]*)?$/,'.utf8$1'),bytes,'text/plain;charset=utf-8');});
|
||
|
||
loadStatus();
|
||
loadApps();
|