initial commit

This commit is contained in:
kittentm 2026-07-24 01:36:58 +02:00
commit b61931d8a1
Signed by: kitten
GPG key ID: 394B4EABE4405A83
17 changed files with 5200 additions and 0 deletions

469
templates/admin.html.tera Normal file
View file

@ -0,0 +1,469 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CDN Manager Control Panel</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
.archivo-black { font-family: "Archivo Black", sans-serif; font-weight: 400; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
width: 100vw;
background-color: #1d1e1f;
color: #c9d1d9;
padding: 2rem 1rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.settings-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 1280px;
margin: 0 auto;
width: 100%;
}
.settings-sidebar {
background-color: #1a1a1f;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
overflow: hidden;
height: max-content;
}
.sidebar-header {
padding: 0.85rem 1rem;
font-size: 1.1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background-color: rgba(0, 0, 0, 0.15);
color: #ffffff;
}
.sidebar-menu { display: flex; flex-direction: column; }
.nav-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: transparent;
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
color: #8b949e;
text-align: left;
font-size: 0.95rem;
width: 100%;
}
.active-nav {
background: rgba(0, 174, 239, 0.1);
border-left: 3px solid #00aeef;
color: #00aeef;
font-weight: 600;
padding-left: calc(1rem - 3px);
}
.settings-main-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.settings-panel {
background-color: #1a1a1f;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
overflow: hidden;
}
.panel-header {
padding: 0.85rem 1.25rem;
font-size: 1.1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background-color: rgba(0, 0, 0, 0.15);
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-body { padding: 1.5rem; }
.form-group { display: flex; gap: 10px; margin-bottom: 1.5rem; }
input[type="text"] {
flex: 1;
padding: 0.6rem 0.8rem;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.2);
color: #ffffff;
font-size: 0.95rem;
outline: none;
}
input[type="text"]:focus { border-color: #00aeef; }
button, .page-btn {
padding: 0.6rem 1.2rem;
background: #00aeef;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
display: inline-block;
transition: opacity 0.2s;
}
button:hover, .page-btn:hover { opacity: 0.9; }
.page-btn.disabled {
background: rgba(255, 255, 255, 0.05);
color: #64748b;
cursor: not-allowed;
pointer-events: none;
}
button.danger {
background: rgba(248, 81, 73, 0.2);
color: #f85149;
border: 1px solid rgba(248, 81, 73, 0.4);
}
button.danger:hover { background: rgba(248, 81, 73, 0.3); }
.alert-box {
background-color: rgba(0, 174, 239, 0.1);
border-left: 4px solid #00aeef;
border-radius: 0 6px 6px 0;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
}
.alert-box h3 { color: #ffffff; font-size: 1rem; margin-bottom: 0.5rem; }
.alert-box code {
display: block;
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
color: #00aeef;
font-family: monospace;
word-break: break-all;
}
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
th, td {
text-align: left;
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
th { background-color: rgba(0, 0, 0, 0.2); color: #8b949e; font-weight: 600; }
td code { font-family: monospace; color: #00aeef; }
.version-tag {
font-size: 0.85rem;
background-color: rgba(0, 174, 239, 0.15);
color: #00aeef;
padding: 0.2rem 0.6rem;
border-radius: 4px;
border: 1px solid rgba(0, 174, 239, 0.3);
font-family: monospace;
}
.status-badge {
font-size: 0.8rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.status-badge.active {
background-color: rgba(74, 222, 128, 0.15);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.3);
}
.status-badge.revoked {
background-color: rgba(248, 81, 73, 0.15);
color: #f85149;
border: 1px solid rgba(248, 81, 73, 0.3);
}
.geo-tag {
font-size: 0.85rem;
color: #8b949e;
font-style: italic;
}
.pagination-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
font-size: 0.88rem;
color: #8b949e;
}
.pagination-nav {
display: flex;
gap: 0.5rem;
align-items: center;
}
.login-wrapper { max-width: 420px; margin: 4rem auto; width: 100%; }
@media (min-width: 768px) {
.settings-container { flex-direction: row; align-items: flex-start; }
.settings-sidebar { width: 280px; flex-shrink: 0; }
}
</style>
</head>
<body>
{% if key %}
<div class="settings-container">
<aside class="settings-sidebar">
<div class="sidebar-header archivo-black">
CONTROL PANEL
</div>
<nav class="sidebar-menu">
<div class="nav-item active-nav"><span>Key Management</span></div>
<div class="nav-item"><span>Stats</span></div>
<div class="nav-item"><span>Bucket Objects</span></div>
</nav>
</aside>
<main class="settings-main-content">
{% if generated_private_key %}
<div class="alert-box">
<h3>Key Pair Generated!</h3>
<p>Copy this private key now. It will not be shown again:</p>
<code>ED25519_PRIVATE_KEY={{ generated_private_key }}</code>
</div>
{% endif %}
<section class="settings-panel">
<div class="panel-header archivo-black">
<span>API KEY MANAGEMENT</span>
</div>
<div class="panel-body">
<form action="/admin/keys/create" method="POST" class="form-group">
<input type="hidden" name="key" value="{{ key }}">
<input type="text" name="label" placeholder="App / Client Label" required>
<button type="submit">Generate Key Pair</button>
</form>
<table>
<thead>
<tr>
<th>Label / ID</th>
<th>Public Key</th>
<th>Created By IP</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for k in api_keys %}
<tr>
<td>
<strong>{{ k.label }}</strong><br>
<small style="color: #8b949e;">{{ k.id }}</small>
</td>
<td><code>{{ k.public_key_hex }}</code></td>
<td>
<span class="ip-address">{{ k.created_by_ip }}</span><br>
<span class="geo-location geo-tag" data-ip="{{ k.created_by_ip }}">Resolving location...</span>
</td>
<td>
{% if k.active %}
<span class="status-badge active">Active</span>
{% else %}
<span class="status-badge revoked">Revoked</span>
{% endif %}
</td>
<td>
{% if k.active %}
<form action="/admin/keys/revoke" method="POST" style="margin: 0;">
<input type="hidden" name="key" value="{{ key }}">
<input type="hidden" name="key_id" value="{{ k.id }}">
<button type="submit" class="danger">Revoke</button>
</form>
{% else %}
<span style="color: #64748b; font-size: 0.85rem;">None</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="5" style="color: #8b949e;">No API keys registered yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<section class="settings-panel">
<div class="panel-header archivo-black">
<span>STATS</span>
<span class="version-tag">Total Visits: {{ total_visits }}</span>
</div>
<div class="panel-body">
<table>
<thead>
<tr>
<th>IP Address</th>
<th>Geolocation</th>
<th>Accessed Endpoint</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td><code>{{ log.ip }}</code></td>
<td><span class="geo-location geo-tag" data-ip="{{ log.ip }}">Resolving location...</span></td>
<td><span style="color: #4ade80;">{{ log.endpoint }}</span></td>
</tr>
{% else %}
<tr>
<td colspan="3" style="color: #8b949e;">No access logs recorded yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<section class="settings-panel">
<div class="panel-header archivo-black">
<span>BUCKET OBJECTS</span>
<span class="version-tag">{{ total_items }} Total Objects</span>
</div>
<div class="panel-body">
<table>
<thead>
<tr>
<th>Object Key</th>
</tr>
</thead>
<tbody>
{% for file_key in keys %}
<tr>
<td><code>{{ file_key }}</code></td>
</tr>
{% else %}
<tr>
<td style="color: #8b949e;">No assets uploaded yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pagination-wrapper">
<div>
Page <strong>{{ current_page }}</strong> of <strong>{{ total_pages }}</strong>
</div>
<div class="pagination-nav">
{% if has_prev %}
<a href="/admin?key={{ key }}&page={{ prev_page }}&per_page={{ per_page }}" class="page-btn">&laquo; Previous</a>
{% else %}
<span class="page-btn disabled">&laquo; Previous</span>
{% endif %}
{% if has_next %}
<a href="/admin?key={{ key }}&page={{ next_page }}&per_page={{ per_page }}" class="page-btn">Next &raquo;</a>
{% else %}
<span class="page-btn disabled">Next &raquo;</span>
{% endif %}
</div>
</div>
</div>
</section>
</main>
</div>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const geoElements = document.querySelectorAll(".geo-location");
const cache = {};
for (const el of geoElements) {
const ip = el.getAttribute("data-ip");
if (!ip || ip === "127.0.0.1" || ip === "::1" || ip.startsWith("192.168.") || ip.startsWith("10.")) {
el.innerText = "On server";
continue;
}
if (cache[ip]) {
el.innerText = cache[ip];
continue;
}
try {
const response = await fetch(`http://ip-api.com/json/${ip}?fields=status,country,city`);
const data = await response.json();
if (data.status === "success") {
const locationStr = `${data.city}, ${data.country}`;
cache[ip] = locationStr;
el.innerText = locationStr;
} else {
el.innerText = "Unknown Location";
}
} catch (e) {
el.innerText = "Location N/A";
}
}
});
</script>
{% else %}
<div class="login-wrapper">
<section class="settings-panel">
<div class="panel-header archivo-black">
ADMIN LOGIN
</div>
<div class="panel-body">
{% if error %}
<p style="color: #f85149; margin-bottom: 1rem; font-size: 0.9rem;">Invalid Key</p>
{% endif %}
<p style="color: #8b949e; font-size: 0.9rem; margin-bottom: 1rem;">
Check Console for key
</p>
<form action="/admin" method="GET">
<input type="text" name="key" placeholder="Enter console access key..." required style="width: 100%; margin-bottom: 1rem;">
<button type="submit" style="width: 100%;">Access Dashboard</button>
</form>
</div>
</section>
</div>
{% endif %}
</body>
</html>