initial commit
This commit is contained in:
commit
b61931d8a1
17 changed files with 5200 additions and 0 deletions
469
templates/admin.html.tera
Normal file
469
templates/admin.html.tera
Normal 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">« Previous</a>
|
||||
{% else %}
|
||||
<span class="page-btn disabled">« Previous</span>
|
||||
{% endif %}
|
||||
|
||||
{% if has_next %}
|
||||
<a href="/admin?key={{ key }}&page={{ next_page }}&per_page={{ per_page }}" class="page-btn">Next »</a>
|
||||
{% else %}
|
||||
<span class="page-btn disabled">Next »</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>
|
||||
Loading…
Reference in a new issue