Merge pull request #123 from BlessedRebuS/feat/ip-tracking-panel
Feat/ip tracking panel
This commit is contained in:
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: krawl-chart
|
||||
description: A Helm chart for Krawl honeypot server
|
||||
type: application
|
||||
version: 1.1.5
|
||||
appVersion: 1.1.5
|
||||
version: 1.1.6
|
||||
appVersion: 1.1.6
|
||||
keywords:
|
||||
- honeypot
|
||||
- security
|
||||
|
||||
106
src/database.py
106
src/database.py
@@ -34,6 +34,7 @@ from models import (
|
||||
AttackDetection,
|
||||
IpStats,
|
||||
CategoryHistory,
|
||||
TrackedIp,
|
||||
)
|
||||
from sanitizer import (
|
||||
sanitize_ip,
|
||||
@@ -2328,6 +2329,111 @@ class DatabaseManager:
|
||||
finally:
|
||||
self.close_session()
|
||||
|
||||
# ── IP Tracking ──────────────────────────────────────────────────
|
||||
|
||||
def track_ip(self, ip: str) -> bool:
|
||||
"""Add an IP to the tracked list with a snapshot of its current stats."""
|
||||
session = self.session
|
||||
sanitized_ip = sanitize_ip(ip)
|
||||
existing = session.query(TrackedIp).filter(TrackedIp.ip == sanitized_ip).first()
|
||||
if existing:
|
||||
return True # already tracked
|
||||
|
||||
# Snapshot essential data from ip_stats
|
||||
stats = session.query(IpStats).filter(IpStats.ip == sanitized_ip).first()
|
||||
tracked = TrackedIp(
|
||||
ip=sanitized_ip,
|
||||
tracked_since=datetime.now(),
|
||||
category=stats.category if stats else None,
|
||||
total_requests=stats.total_requests if stats else 0,
|
||||
country_code=stats.country_code if stats else None,
|
||||
city=stats.city if stats else None,
|
||||
last_seen=stats.last_seen if stats else None,
|
||||
)
|
||||
session.add(tracked)
|
||||
try:
|
||||
session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
applogger.error(f"Error tracking IP {sanitized_ip}: {e}")
|
||||
return False
|
||||
|
||||
def untrack_ip(self, ip: str) -> bool:
|
||||
"""Remove an IP from the tracked list."""
|
||||
session = self.session
|
||||
sanitized_ip = sanitize_ip(ip)
|
||||
tracked = session.query(TrackedIp).filter(TrackedIp.ip == sanitized_ip).first()
|
||||
if not tracked:
|
||||
return False
|
||||
session.delete(tracked)
|
||||
try:
|
||||
session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
applogger.error(f"Error untracking IP {sanitized_ip}: {e}")
|
||||
return False
|
||||
|
||||
def is_ip_tracked(self, ip: str) -> bool:
|
||||
"""Check if an IP is currently tracked."""
|
||||
session = self.session
|
||||
sanitized_ip = sanitize_ip(ip)
|
||||
try:
|
||||
return (
|
||||
session.query(TrackedIp).filter(TrackedIp.ip == sanitized_ip).first()
|
||||
is not None
|
||||
)
|
||||
finally:
|
||||
self.close_session()
|
||||
|
||||
def get_tracked_ips_paginated(
|
||||
self,
|
||||
page: int = 1,
|
||||
page_size: int = 25,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get all tracked IPs, paginated. Reads only from tracked_ips table."""
|
||||
session = self.session
|
||||
try:
|
||||
total = session.query(TrackedIp).count()
|
||||
total_pages = max(1, (total + page_size - 1) // page_size)
|
||||
|
||||
tracked_rows = (
|
||||
session.query(TrackedIp)
|
||||
.order_by(TrackedIp.tracked_since.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
items = []
|
||||
for t in tracked_rows:
|
||||
items.append(
|
||||
{
|
||||
"ip": t.ip,
|
||||
"tracked_since": (
|
||||
t.tracked_since.isoformat() if t.tracked_since else None
|
||||
),
|
||||
"category": t.category,
|
||||
"total_requests": t.total_requests or 0,
|
||||
"country_code": t.country_code,
|
||||
"city": t.city,
|
||||
"last_seen": t.last_seen.isoformat() if t.last_seen else None,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"tracked_ips": items,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"total_pages": total_pages,
|
||||
},
|
||||
}
|
||||
finally:
|
||||
self.close_session()
|
||||
|
||||
|
||||
# Module-level singleton instance
|
||||
_db_manager = DatabaseManager()
|
||||
|
||||
@@ -244,6 +244,32 @@ class CategoryHistory(Base):
|
||||
return f"<CategoryHistory(ip='{self.ip}', {self.old_category} -> {self.new_category})>"
|
||||
|
||||
|
||||
class TrackedIp(Base):
|
||||
"""
|
||||
Manually tracked IP addresses for monitoring.
|
||||
|
||||
Stores a snapshot of essential ip_stats data at tracking time
|
||||
so the tracked IPs panel never needs to query the large ip_stats table.
|
||||
"""
|
||||
|
||||
__tablename__ = "tracked_ips"
|
||||
|
||||
ip: Mapped[str] = mapped_column(String(MAX_IP_LENGTH), primary_key=True)
|
||||
tracked_since: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, default=datetime.utcnow
|
||||
)
|
||||
|
||||
# Snapshot from ip_stats at tracking time
|
||||
category: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
total_requests: Mapped[int] = mapped_column(Integer, default=0, nullable=True)
|
||||
country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
|
||||
city: Mapped[Optional[str]] = mapped_column(String(MAX_CITY_LENGTH), nullable=True)
|
||||
last_seen: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TrackedIp(ip='{self.ip}')>"
|
||||
|
||||
|
||||
# class IpLog(Base):
|
||||
# """
|
||||
# Records all IPs that have accessed the honeypot, along with aggregated stats and inferred user category.
|
||||
|
||||
@@ -171,6 +171,38 @@ async def ban_override(request: Request, body: BanOverrideRequest):
|
||||
return JSONResponse(content={"error": "IP not found"}, status_code=404)
|
||||
|
||||
|
||||
# ── Protected IP Tracking API ────────────────────────────────────────
|
||||
|
||||
|
||||
class TrackIpRequest(BaseModel):
|
||||
ip: str
|
||||
action: str # "track" or "untrack"
|
||||
|
||||
|
||||
@router.post("/api/track-ip")
|
||||
async def track_ip(request: Request, body: TrackIpRequest):
|
||||
if not verify_auth(request):
|
||||
return JSONResponse(content={"error": "Unauthorized"}, status_code=401)
|
||||
|
||||
db = get_db()
|
||||
if body.action == "track":
|
||||
success = db.track_ip(body.ip)
|
||||
elif body.action == "untrack":
|
||||
success = db.untrack_ip(body.ip)
|
||||
else:
|
||||
return JSONResponse(
|
||||
content={"error": "Invalid action. Use: track, untrack"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if success:
|
||||
get_app_logger().info(f"IP tracking: {body.action} on IP {body.ip}")
|
||||
return JSONResponse(
|
||||
content={"success": True, "ip": body.ip, "action": body.action}
|
||||
)
|
||||
return JSONResponse(content={"error": "IP not found"}, status_code=404)
|
||||
|
||||
|
||||
@router.get("/api/all-ip-stats")
|
||||
async def all_ip_stats(request: Request):
|
||||
db = get_db()
|
||||
|
||||
@@ -343,6 +343,8 @@ async def htmx_ip_insight(ip_address: str, request: Request):
|
||||
stats["blocklist_memberships"] = list(list_on.keys()) if list_on else []
|
||||
stats["reverse_dns"] = stats.get("reverse")
|
||||
|
||||
is_tracked = db.is_ip_tracked(ip_address)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/ip_insight.html",
|
||||
@@ -351,6 +353,7 @@ async def htmx_ip_insight(ip_address: str, request: Request):
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"stats": stats,
|
||||
"ip_address": ip_address,
|
||||
"is_tracked": is_tracked,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -371,6 +374,8 @@ async def htmx_ip_detail(ip_address: str, request: Request):
|
||||
stats["blocklist_memberships"] = list(list_on.keys()) if list_on else []
|
||||
stats["reverse_dns"] = stats.get("reverse")
|
||||
|
||||
is_tracked = db.is_ip_tracked(ip_address)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/ip_detail.html",
|
||||
@@ -378,6 +383,7 @@ async def htmx_ip_detail(ip_address: str, request: Request):
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"stats": stats,
|
||||
"is_tracked": is_tracked,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -464,6 +470,55 @@ async def htmx_ban_attackers(
|
||||
)
|
||||
|
||||
|
||||
# ── Protected Tracked IPs Panel ──────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/tracked-ips")
|
||||
async def htmx_tracked_ips(request: Request):
|
||||
if not verify_auth(request):
|
||||
return HTMLResponse(
|
||||
'<div class="table-container" style="text-align:center;padding:80px 20px;">'
|
||||
'<h1 style="color:#f0883e;font-size:48px;margin:20px 0 10px;">Nice try bozo</h1>'
|
||||
"<br>"
|
||||
'<img src="https://media0.giphy.com/media/v1.Y2lkPTZjMDliOTUyaHQ3dHRuN2wyOW1kZndjaHdkY2dhYzJ6d2gzMDJkNm53ZnNrdnNlZCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/mOY97EXNisstZqJht9/200w.gif" alt="Diddy">'
|
||||
"</div>",
|
||||
status_code=200,
|
||||
)
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/tracked_ips_panel.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/htmx/tracked-ips/list")
|
||||
async def htmx_tracked_ips_list(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(25),
|
||||
):
|
||||
if not verify_auth(request):
|
||||
return HTMLResponse(
|
||||
"<p style='color:#f85149;'>Unauthorized</p>", status_code=200
|
||||
)
|
||||
|
||||
db = get_db()
|
||||
result = db.get_tracked_ips_paginated(page=max(1, page), page_size=page_size)
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/tracked_ips_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["tracked_ips"],
|
||||
"pagination": result["pagination"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/htmx/ban/overrides")
|
||||
async def htmx_ban_overrides(
|
||||
request: Request,
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
<a class="tab-button" :class="{ active: tab === 'ip-insight', disabled: !insightIp }" @click.prevent="insightIp && switchToIpInsight()" href="#ip-insight">
|
||||
IP Insight<span x-show="insightIp" x-text="' (' + insightIp + ')'"></span>
|
||||
</a>
|
||||
<a class="tab-button tab-right" :class="{ active: tab === 'banlist' }" x-show="authenticated" x-cloak @click.prevent="switchToBanlist()" href="#banlist">IP Banlist</a>
|
||||
<a class="tab-button tab-right" :class="{ active: tab === 'tracked-ips' }" x-show="authenticated" x-cloak @click.prevent="switchToTrackedIps()" href="#tracked-ips">Tracked IPs</a>
|
||||
<a class="tab-button" :class="{ active: tab === 'banlist' }" x-show="authenticated" x-cloak @click.prevent="switchToBanlist()" href="#banlist">IP Banlist</a>
|
||||
{# Lock icon (not authenticated) #}
|
||||
<a class="tab-button tab-lock-btn" :class="{ 'tab-right': !authenticated }" @click.prevent="promptAuth()" x-show="!authenticated" href="#" title="Unlock protected panels">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
@@ -197,6 +198,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ==================== TRACKED IPS TAB (protected, loaded via HTMX with server-side auth) ==================== #}
|
||||
<div x-show="tab === 'tracked-ips'" x-cloak>
|
||||
<div id="tracked-ips-htmx-container"></div>
|
||||
</div>
|
||||
|
||||
{# ==================== IP BANLIST TAB (protected, loaded via HTMX with server-side auth) ==================== #}
|
||||
<div x-show="tab === 'banlist'" x-cloak>
|
||||
<div id="banlist-htmx-container"></div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{# Ban/Unban actions — visible only when authenticated #}
|
||||
{# Ban/Unban + Track/Untrack actions — visible only when authenticated #}
|
||||
<span class="ip-ban-actions" style="display: none; gap: 4px;">
|
||||
{% if stats.category and stats.category | lower == 'attacker' %}
|
||||
<button class="ban-icon-btn ban-icon-unban" onclick="ipBanAction('{{ ip_address | e }}', 'unban')" title="Unban">
|
||||
@@ -25,6 +25,17 @@
|
||||
<span class="ban-icon-tooltip">Add to banlist</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if is_tracked %}
|
||||
<button class="ban-icon-btn track-icon-untrack" onclick="ipTrackAction('{{ ip_address | e }}', 'untrack')" title="Untrack IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
|
||||
<span class="ban-icon-tooltip">Untrack</span>
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="ban-icon-btn track-icon-track" onclick="ipTrackAction('{{ ip_address | e }}', 'track')" title="Track IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
<span class="ban-icon-tooltip">Track</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if stats.city or stats.country %}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
{# IP Tracking management panel #}
|
||||
<div x-data="trackManagement()" x-init="init()">
|
||||
|
||||
{# Track IP form #}
|
||||
<div class="table-container" style="margin-bottom: 20px;">
|
||||
<h2>Tracked IPs</h2>
|
||||
<p style="color: #8b949e; font-size: 14px; margin-bottom: 16px;">
|
||||
Track an IP address to monitor its activity. You can also track IPs from the IP Insight page.
|
||||
</p>
|
||||
<form @submit.prevent="trackIp()" style="display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;">
|
||||
<div style="flex: 1; min-width: 200px;">
|
||||
<label style="display: block; color: #8b949e; font-size: 12px; margin-bottom: 4px;">IP Address</label>
|
||||
<input type="text"
|
||||
x-model="newTrackIp"
|
||||
placeholder="e.g. 192.168.1.100"
|
||||
class="auth-modal-input"
|
||||
style="width: 100%;" />
|
||||
</div>
|
||||
<button type="submit" class="track-form-btn" :disabled="!newTrackIp || trackLoading">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
<span x-text="trackLoading ? 'Tracking...' : 'Track IP'"></span>
|
||||
</button>
|
||||
</form>
|
||||
<p x-show="trackMessage" x-text="trackMessage" :style="{ color: trackSuccess ? '#3fb950' : '#f85149' }" style="margin-top: 10px; font-size: 13px;" x-cloak></p>
|
||||
</div>
|
||||
|
||||
{# Tracked IPs list #}
|
||||
<div class="table-container">
|
||||
<h2>Currently Tracked</h2>
|
||||
<div id="tracked-ips-container"
|
||||
class="htmx-container"
|
||||
hx-get="{{ dashboard_path }}/htmx/tracked-ips/list?page=1"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="htmx-indicator">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('trackManagement', () => ({
|
||||
newTrackIp: '',
|
||||
trackLoading: false,
|
||||
trackMessage: '',
|
||||
trackSuccess: false,
|
||||
|
||||
init() {},
|
||||
|
||||
async trackIp() {
|
||||
if (!this.newTrackIp) return;
|
||||
this.trackLoading = true;
|
||||
this.trackMessage = '';
|
||||
try {
|
||||
const resp = await fetch(`${window.__DASHBOARD_PATH__}/api/track-ip`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ ip: this.newTrackIp, action: 'track' }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (resp.ok) {
|
||||
this.trackSuccess = true;
|
||||
this.trackMessage = `IP ${this.newTrackIp} is now being tracked`;
|
||||
this.newTrackIp = '';
|
||||
this.refreshList();
|
||||
} else {
|
||||
this.trackSuccess = false;
|
||||
this.trackMessage = data.error || 'Failed to track IP';
|
||||
}
|
||||
} catch {
|
||||
this.trackSuccess = false;
|
||||
this.trackMessage = 'Request failed';
|
||||
}
|
||||
this.trackLoading = false;
|
||||
},
|
||||
|
||||
refreshList() {
|
||||
const container = document.getElementById('tracked-ips-container');
|
||||
if (container && typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', `${window.__DASHBOARD_PATH__}/htmx/tracked-ips/list?page=1`, {
|
||||
target: '#tracked-ips-container',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
{# HTMX fragment: Tracked IPs list #}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<span class="pagination-info">Page {{ pagination.page }}/{{ pagination.total_pages }} — {{ pagination.total }} tracked</span>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="pagination-btn"
|
||||
hx-get="{{ dashboard_path }}/htmx/tracked-ips/list?page={{ pagination.page - 1 }}"
|
||||
hx-target="closest .htmx-container"
|
||||
hx-swap="innerHTML"
|
||||
{% if pagination.page <= 1 %}disabled{% endif %}>Prev</button>
|
||||
<button class="pagination-btn"
|
||||
hx-get="{{ dashboard_path }}/htmx/tracked-ips/list?page={{ pagination.page + 1 }}"
|
||||
hx-target="closest .htmx-container"
|
||||
hx-swap="innerHTML"
|
||||
{% if pagination.page >= pagination.total_pages %}disabled{% endif %}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ip-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>IP Address</th>
|
||||
<th>Category</th>
|
||||
<th>Total Requests</th>
|
||||
<th>Location</th>
|
||||
<th>Last Seen</th>
|
||||
<th>Tracked Since</th>
|
||||
<th style="width: 80px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ip in items %}
|
||||
<tr>
|
||||
<td class="rank">{{ loop.index + (pagination.page - 1) * pagination.page_size }}</td>
|
||||
<td>{{ ip.ip | e }}</td>
|
||||
<td><span class="category-badge category-{{ ip.category | default('unknown') | replace('_', '-') }}">{{ ip.category | default('unknown') | replace('_', ' ') | title }}</span></td>
|
||||
<td>{{ ip.total_requests }}</td>
|
||||
<td>{{ ip.city | default('') | e }}{% if ip.city and ip.country_code %}, {% endif %}{{ ip.country_code | default('N/A') | e }}</td>
|
||||
<td>{{ ip.last_seen | format_ts }}</td>
|
||||
<td>{{ ip.tracked_since | format_ts }}</td>
|
||||
<td style="display: flex; gap: 4px;">
|
||||
<button class="ban-icon-btn track-icon-inspect" onclick="openIpInsight('{{ ip.ip | e }}')" title="Inspect">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
<span class="ban-icon-tooltip">Inspect</span>
|
||||
</button>
|
||||
<button class="ban-icon-btn track-icon-untrack" onclick="ipTrackAction('{{ ip.ip | e }}', 'untrack')" title="Untrack">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
|
||||
<span class="ban-icon-tooltip">Untrack</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="empty-state">No tracked IPs</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1338,7 +1338,7 @@ tbody {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
padding: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
@@ -1346,12 +1346,12 @@ tbody {
|
||||
transition: color 0.2s, background 0.2s;
|
||||
}
|
||||
.ban-icon-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
.ban-icon-btn .material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
font-size: 26px;
|
||||
}
|
||||
.ban-icon-unban {
|
||||
color: #3fb950;
|
||||
@@ -1392,6 +1392,53 @@ tbody {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* IP Tracking buttons */
|
||||
.track-form-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 18px;
|
||||
background: rgba(88, 166, 255, 0.15);
|
||||
color: #58a6ff;
|
||||
border: 1px solid rgba(88, 166, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.track-form-btn:hover:not(:disabled) {
|
||||
background: rgba(88, 166, 255, 0.3);
|
||||
border-color: rgba(88, 166, 255, 0.5);
|
||||
}
|
||||
.track-form-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.track-form-btn .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
}
|
||||
.track-icon-track {
|
||||
color: #58a6ff;
|
||||
}
|
||||
.track-icon-track:hover {
|
||||
background: rgba(88, 166, 255, 0.15);
|
||||
}
|
||||
.track-icon-untrack {
|
||||
color: #8b949e;
|
||||
}
|
||||
.track-icon-untrack:hover {
|
||||
color: #c9d1d9;
|
||||
background: rgba(139, 148, 158, 0.15);
|
||||
}
|
||||
.track-icon-inspect {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
.track-icon-inspect:hover {
|
||||
background: rgba(210, 168, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Custom confirm/alert modal */
|
||||
.krawl-modal-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -48,6 +48,8 @@ document.addEventListener('alpine:init', () => {
|
||||
this.switchToAttacks();
|
||||
} else if (h === 'banlist') {
|
||||
if (this.authenticated) this.switchToBanlist();
|
||||
} else if (h === 'tracked-ips') {
|
||||
if (this.authenticated) this.switchToTrackedIps();
|
||||
} else if (h !== 'ip-insight') {
|
||||
if (this.tab !== 'ip-insight') {
|
||||
this.switchToOverview();
|
||||
@@ -91,6 +93,21 @@ document.addEventListener('alpine:init', () => {
|
||||
});
|
||||
},
|
||||
|
||||
switchToTrackedIps() {
|
||||
if (!this.authenticated) return;
|
||||
this.tab = 'tracked-ips';
|
||||
window.location.hash = '#tracked-ips';
|
||||
this.$nextTick(() => {
|
||||
const container = document.getElementById('tracked-ips-htmx-container');
|
||||
if (container && typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', `${this.dashboardPath}/htmx/tracked-ips`, {
|
||||
target: '#tracked-ips-htmx-container',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await fetch(`${this.dashboardPath}/api/auth/logout`, {
|
||||
@@ -99,7 +116,7 @@ document.addEventListener('alpine:init', () => {
|
||||
});
|
||||
} catch {}
|
||||
this.authenticated = false;
|
||||
if (this.tab === 'banlist') this.switchToOverview();
|
||||
if (this.tab === 'banlist' || this.tab === 'tracked-ips') this.switchToOverview();
|
||||
},
|
||||
|
||||
promptAuth() {
|
||||
@@ -357,6 +374,43 @@ window.ipBanAction = async function(ip, action) {
|
||||
}
|
||||
};
|
||||
|
||||
// Global track action for IP insight page (auth-gated)
|
||||
window.ipTrackAction = async function(ip, action) {
|
||||
const data = getAlpineData('[x-data="dashboardApp()"]');
|
||||
if (!data || !data.authenticated) {
|
||||
if (data && typeof data.promptAuth === 'function') data.promptAuth();
|
||||
return;
|
||||
}
|
||||
const safeIp = escapeHtml(ip);
|
||||
const label = action === 'track' ? 'track' : 'untrack';
|
||||
const confirmed = await krawlModal.confirm(`Are you sure you want to ${label} IP <strong>${safeIp}</strong>?`);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const resp = await fetch(`${window.__DASHBOARD_PATH__}/api/track-ip`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ ip, action }),
|
||||
});
|
||||
const result = await resp.json().catch(() => ({}));
|
||||
if (resp.ok) {
|
||||
krawlModal.success(escapeHtml(result.message || `${label} successful for ${ip}`));
|
||||
// Refresh tracked IPs list if visible
|
||||
const container = document.getElementById('tracked-ips-container');
|
||||
if (container && typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', `${window.__DASHBOARD_PATH__}/htmx/tracked-ips/list?page=1`, {
|
||||
target: '#tracked-ips-container',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
krawlModal.error(escapeHtml(result.error || `Failed to ${label} IP ${ip}`));
|
||||
}
|
||||
} catch {
|
||||
krawlModal.error('Request failed');
|
||||
}
|
||||
};
|
||||
|
||||
// Show/hide ban action buttons based on auth state
|
||||
function updateBanActionVisibility(authenticated) {
|
||||
document.querySelectorAll('.ip-ban-actions').forEach(el => {
|
||||
|
||||
Reference in New Issue
Block a user