Autarch/web/templates/ipcapture.html

234 lines
12 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block title %}IP Capture — AUTARCH{% endblock %}
{% block content %}
<h1>IP Capture &amp; Redirect</h1>
<p style="color:var(--text-secondary);margin-bottom:1.5rem">
Create stealthy tracking links that capture visitor IP + metadata, then redirect to a legitimate site.
</p>
<!-- Tabs -->
<div class="tabs" style="display:flex;gap:0;border-bottom:2px solid var(--border);margin-bottom:1.5rem">
<button class="tab-btn active" onclick="capTab('create',this)">Create &amp; Manage</button>
<button class="tab-btn" onclick="capTab('captures',this)">Captures</button>
</div>
<!-- ═══════════════════ CREATE TAB ═══════════════════ -->
<div id="tab-create" class="tab-pane">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem">
<!-- Create link -->
<div class="card" style="padding:1.25rem">
<h3 style="margin-bottom:1rem">Create Capture Link</h3>
<label class="form-label">Target URL (redirect destination)</label>
<input id="cap-target" class="form-input" placeholder="https://example.com/real-article">
<label class="form-label" style="margin-top:0.5rem">Friendly Name</label>
<input id="cap-name" class="form-input" placeholder="Phishing awareness test #1">
<label class="form-label" style="margin-top:0.5rem">Disguise Type</label>
<select id="cap-disguise" class="form-input">
<option value="article">Article URL (realistic path)</option>
<option value="short">Short URL (/c/xxxxx)</option>
</select>
<button class="btn btn-primary" style="margin-top:1rem;width:100%" onclick="createCapLink()">Create Link</button>
<!-- Result -->
<div id="cap-result" style="display:none;margin-top:1rem;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:1rem">
<div style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.5rem">Your tracking links:</div>
<div style="margin-bottom:0.4rem">
<label style="font-size:0.72rem;color:var(--text-muted)">SHORT URL</label>
<div style="display:flex;gap:0.4rem;align-items:center">
<input id="cap-res-short" class="form-input" readonly style="font-family:monospace;font-size:0.82rem">
<button class="btn btn-small" onclick="copyUrl('cap-res-short')">Copy</button>
</div>
</div>
<div>
<label style="font-size:0.72rem;color:var(--text-muted)">ARTICLE URL</label>
<div style="display:flex;gap:0.4rem;align-items:center">
<input id="cap-res-article" class="form-input" readonly style="font-family:monospace;font-size:0.82rem">
<button class="btn btn-small" onclick="copyUrl('cap-res-article')">Copy</button>
</div>
</div>
</div>
</div>
<!-- Active links -->
<div class="card" style="padding:1.25rem">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h3>Active Links</h3>
<button class="btn btn-small" onclick="loadCapLinks()">Refresh</button>
</div>
<div id="cap-links" style="font-size:0.85rem">Loading...</div>
</div>
</div>
</div>
<!-- ═══════════════════ CAPTURES TAB ═══════════════════ -->
<div id="tab-captures" class="tab-pane" style="display:none">
<div class="card" style="padding:1.25rem">
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem">
<h3>Captures for:</h3>
<select id="cap-select" class="form-input" style="width:auto;min-width:250px" onchange="loadCaptures()"></select>
<button class="btn btn-small" onclick="loadCaptures()">Refresh</button>
<div style="margin-left:auto;display:flex;gap:0.4rem">
<button class="btn btn-small" onclick="exportCap('json')">Export JSON</button>
<button class="btn btn-small" onclick="exportCap('csv')">Export CSV</button>
</div>
</div>
<div id="cap-stats" style="display:flex;gap:1.5rem;margin-bottom:1rem;font-size:0.9rem"></div>
<table style="width:100%;font-size:0.82rem;border-collapse:collapse">
<thead>
<tr style="border-bottom:2px solid var(--border);text-align:left">
<th style="padding:6px">IP</th>
<th style="padding:6px">Timestamp</th>
<th style="padding:6px">Location</th>
<th style="padding:6px">User Agent</th>
<th style="padding:6px">Language</th>
</tr>
</thead>
<tbody id="cap-table"></tbody>
</table>
</div>
</div>
<style>
.tab-btn{padding:0.6rem 1.2rem;background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:0.9rem;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all 0.2s}
.tab-btn:hover{color:var(--text-primary)}
.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}
.form-label{display:block;font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.25rem;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}
.form-input{width:100%;padding:0.5rem 0.65rem;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-primary);font-size:0.85rem}
.form-input:focus{outline:none;border-color:var(--accent)}
.btn-danger{background:var(--danger);color:#fff}
.link-card{background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;margin-bottom:0.5rem}
.link-card:hover{border-color:var(--accent)}
</style>
<script>
let _currentCapKey = '';
function capTab(name, btn) {
document.querySelectorAll('.tab-pane').forEach(p => p.style.display = 'none');
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + name).style.display = '';
btn.classList.add('active');
if (name === 'captures') { loadCapSelect(); loadCaptures(); }
}
function copyUrl(id) {
const el = document.getElementById(id);
el.select();
document.execCommand('copy');
}
// ── Create ──
function createCapLink() {
const data = {
target_url: document.getElementById('cap-target').value,
name: document.getElementById('cap-name').value,
disguise: document.getElementById('cap-disguise').value,
};
fetch('/ipcapture/links', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)})
.then(r => r.json()).then(d => {
if (d.ok) {
const base = window.location.origin;
document.getElementById('cap-res-short').value = base + d.short_path;
document.getElementById('cap-res-article').value = base + d.article_path;
document.getElementById('cap-result').style.display = '';
loadCapLinks();
} else {
alert(d.error);
}
});
}
// ── Links ──
function loadCapLinks() {
fetch('/ipcapture/links').then(r => r.json()).then(d => {
const el = document.getElementById('cap-links');
if (!d.ok || !d.links.length) { el.textContent = 'No links created yet'; return; }
el.innerHTML = d.links.map(l => {
const s = l.stats || {};
return `<div class="link-card">
<div style="display:flex;justify-content:space-between;align-items:center">
<strong>${l.name || l.key}</strong>
<span style="font-size:0.78rem;color:var(--text-muted)">${s.total || 0} captures (${s.unique_ips || 0} unique)</span>
</div>
<div style="font-size:0.78rem;color:var(--text-secondary);margin-top:0.3rem">
Target: <a href="${l.target_url}" target="_blank" style="color:var(--accent)">${l.target_url.substring(0,60)}${l.target_url.length > 60 ? '...' : ''}</a>
</div>
<div style="font-size:0.75rem;font-family:monospace;color:var(--text-muted);margin-top:0.2rem">
${l.short_path} &bull; ${l.article_path || ''}
</div>
<div style="margin-top:0.5rem;display:flex;gap:0.4rem">
<button class="btn btn-small" onclick="_currentCapKey='${l.key}';capTab('captures',document.querySelectorAll('.tab-btn')[1])">View Captures</button>
<button class="btn btn-small btn-danger" onclick="deleteCapLink('${l.key}')">Delete</button>
</div>
</div>`;
}).join('');
});
}
function deleteCapLink(key) {
if (!confirm('Delete this capture link?')) return;
fetch(`/ipcapture/links/${key}`, {method:'DELETE'}).then(() => loadCapLinks());
}
// ── Captures ──
function loadCapSelect() {
fetch('/ipcapture/links').then(r => r.json()).then(d => {
const sel = document.getElementById('cap-select');
if (!d.ok || !d.links.length) { sel.innerHTML = '<option>No links</option>'; return; }
sel.innerHTML = d.links.map(l => `<option value="${l.key}">${l.name || l.key} (${(l.stats||{}).total||0} captures)</option>`).join('');
if (_currentCapKey) sel.value = _currentCapKey;
});
}
function loadCaptures() {
const key = document.getElementById('cap-select').value || _currentCapKey;
if (!key) return;
_currentCapKey = key;
fetch(`/ipcapture/links/${key}`).then(r => r.json()).then(d => {
if (!d.ok) return;
const link = d.link;
const s = link.stats || {};
document.getElementById('cap-stats').innerHTML =
`<span>Total: <strong>${s.total||0}</strong></span>
<span>Unique IPs: <strong>${s.unique_ips||0}</strong></span>
<span style="color:var(--text-muted)">First: ${s.first ? new Date(s.first).toLocaleString() : '—'}</span>
<span style="color:var(--text-muted)">Last: ${s.last ? new Date(s.last).toLocaleString() : '—'}</span>`;
const captures = link.captures || [];
const el = document.getElementById('cap-table');
if (!captures.length) {
el.innerHTML = '<tr><td colspan="5" style="padding:10px;color:var(--text-muted)">No captures yet. Share your link and wait for clicks.</td></tr>';
return;
}
el.innerHTML = captures.map(c => {
const geo = c.geo || {};
const loc = geo.city ? `${geo.city}, ${geo.country}` : (geo.country || 'Unknown');
const ua = (c.user_agent || '').substring(0, 60) + ((c.user_agent||'').length > 60 ? '...' : '');
return `<tr style="border-bottom:1px solid var(--border)">
<td style="padding:5px;font-family:monospace;font-weight:600">${c.ip}</td>
<td style="padding:5px;font-size:0.78rem">${new Date(c.timestamp).toLocaleString()}</td>
<td style="padding:5px">${loc}</td>
<td style="padding:5px;font-size:0.75rem;max-width:250px;overflow:hidden;text-overflow:ellipsis" title="${c.user_agent||''}">${ua}</td>
<td style="padding:5px;font-size:0.75rem">${c.accept_language || ''}</td>
</tr>`;
}).join('');
});
}
function exportCap(fmt) {
const key = _currentCapKey || document.getElementById('cap-select').value;
if (!key) return;
window.open(`/ipcapture/links/${key}/export?format=${fmt}`, '_blank');
}
// Init
document.addEventListener('DOMContentLoaded', loadCapLinks);
</script>
{% endblock %}