feat(admin): add security and activity management features
This commit is contained in:
92
templates/admin/activity.html
Normal file
92
templates/admin/activity.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{{ define "admin/activity.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WarpBox Admin Activity</title>
|
||||
<link rel="icon" type="image/png" href="/static/WarpBoxLogo.png">
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
<link rel="stylesheet" href="/static/css/window.css">
|
||||
<link rel="stylesheet" href="/static/css/components/buttons.css">
|
||||
<link rel="stylesheet" href="/static/css/components/toast.css">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
<link rel="stylesheet" href="/static/css/activity.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-shell">
|
||||
<div class="admin-frame">
|
||||
{{ template "admin/header.html" . }}
|
||||
<div class="win98-window admin-workspace-window" role="main">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
||||
<h1>WarpBox Activity</h1>
|
||||
</div>
|
||||
<div class="win98-window-controls" aria-hidden="true">
|
||||
<button class="win98-control" type="button">_</button>
|
||||
<button class="win98-control" type="button">□</button>
|
||||
<button class="win98-control" type="button">x</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="menu-bar" aria-label="Activity toolbar">
|
||||
<div class="menu-item">
|
||||
<button class="menu-button" type="button" aria-expanded="false">File</button>
|
||||
<div class="menu-popup">
|
||||
<button class="menu-action" type="button" data-command="refresh"><span>R</span><span>Refresh list</span><span>F5</span></button>
|
||||
<button class="menu-action" type="button" data-command="export"><span>E</span><span>Export visible JSON</span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="admin-workspace-body activity-page-body">
|
||||
<section class="activity-panel">
|
||||
<div class="activity-toolbar-grid">
|
||||
<input class="activity-input" id="activity-search" type="search" placeholder="Search kind, message, ip, path">
|
||||
<select class="activity-select" id="activity-severity">
|
||||
<option value="all" selected>All severities</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<select class="activity-select" id="activity-kind">
|
||||
<option value="all" selected>All kinds</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="activity-table-wrap">
|
||||
<table class="activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Kind</th>
|
||||
<th>Severity</th>
|
||||
<th>IP</th>
|
||||
<th>Method</th>
|
||||
<th>Path</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="activity-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="status-bar admin-dashboard-statusbar">
|
||||
<span id="activity-status-left">{{ len .Events }} events loaded</span>
|
||||
<span id="activity-status-middle">retention from settings</span>
|
||||
<span id="activity-status-right">admin only</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
<script id="activity-data" type="application/json">{{ toJSON .Events }}</script>
|
||||
<script src="/static/js/warpbox-ui.js"></script>
|
||||
<script src="/static/js/admin/activity.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
@@ -61,22 +61,22 @@
|
||||
<section class="alerts-summary-grid" aria-label="Alerts summary">
|
||||
<article class="alerts-stat-card is-danger">
|
||||
<p class="alerts-stat-label">Open alerts</p>
|
||||
<p class="alerts-stat-value" data-open-count>5</p>
|
||||
<p class="alerts-stat-value" data-open-count>{{ .OpenCount }}</p>
|
||||
<p class="alerts-stat-note">Requires attention</p>
|
||||
</article>
|
||||
<article class="alerts-stat-card is-warning">
|
||||
<p class="alerts-stat-label">High severity</p>
|
||||
<p class="alerts-stat-value" data-high-count>2</p>
|
||||
<p class="alerts-stat-value" data-high-count>{{ .HighCount }}</p>
|
||||
<p class="alerts-stat-note">Escalate first</p>
|
||||
</article>
|
||||
<article class="alerts-stat-card is-info">
|
||||
<p class="alerts-stat-label">Acknowledged</p>
|
||||
<p class="alerts-stat-value" data-ack-count>3</p>
|
||||
<p class="alerts-stat-value" data-ack-count>{{ .AckCount }}</p>
|
||||
<p class="alerts-stat-note">Seen but not closed</p>
|
||||
</article>
|
||||
<article class="alerts-stat-card is-info">
|
||||
<p class="alerts-stat-label">Closed today</p>
|
||||
<p class="alerts-stat-value" data-closed-count>2</p>
|
||||
<p class="alerts-stat-value" data-closed-count>{{ .ClosedCount }}</p>
|
||||
<p class="alerts-stat-note">History stays lightweight</p>
|
||||
</article>
|
||||
</section>
|
||||
@@ -134,108 +134,7 @@
|
||||
<th class="alerts-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="alerts-body">
|
||||
<tr data-id="10" data-severity="high" data-status="open" data-group="storage" data-title="Storage connector unavailable" data-description="Primary local storage connector failed health check and new writes are paused." data-code="301" data-trace="storage.connector.health_failed" data-time="today 14:08" data-metadata='{"connector":"local-main","mode":"read_only","retry_in":"30s"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Storage connector unavailable</td>
|
||||
<td><span class="alerts-pill high">high</span></td>
|
||||
<td><span class="alerts-pill open">open</span></td>
|
||||
<td>301</td>
|
||||
<td>storage.connector.health_failed</td>
|
||||
<td>today 14:08</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="9" data-severity="medium" data-status="open" data-group="thumbnails" data-title="Thumbnail generation failed" data-description="Thumbnail generation failed for one uploaded image. Original file remains available." data-code="601" data-trace="thumbnail.generate.failed" data-time="today 13:40" data-metadata='{"box":"bx_49aa","file":"poster.png","worker":"thumb-2"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Thumbnail generation failed</td>
|
||||
<td><span class="alerts-pill medium">medium</span></td>
|
||||
<td><span class="alerts-pill open">open</span></td>
|
||||
<td>601</td>
|
||||
<td>thumbnail.generate.failed</td>
|
||||
<td>today 13:40</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="8" data-severity="low" data-status="acked" data-group="uploads" data-title="Large upload nearing account cap" data-description="A user is close to their daily upload budget." data-code="124" data-trace="upload.quota.nearing_cap" data-time="today 12:58" data-metadata='{"user":"geo","used":"44 GB","limit":"50 GB"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Large upload nearing account cap</td>
|
||||
<td><span class="alerts-pill low">low</span></td>
|
||||
<td><span class="alerts-pill acked">acked</span></td>
|
||||
<td>124</td>
|
||||
<td>upload.quota.nearing_cap</td>
|
||||
<td>today 12:58</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="7" data-severity="high" data-status="open" data-group="auth" data-title="Repeated admin login failures" data-description="Multiple failed admin login attempts were detected from the same source." data-code="211" data-trace="auth.admin.failed_login_burst" data-time="today 12:10" data-metadata='{"ip":"198.51.100.4","attempts":7,"window":"10m"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Repeated admin login failures</td>
|
||||
<td><span class="alerts-pill high">high</span></td>
|
||||
<td><span class="alerts-pill open">open</span></td>
|
||||
<td>211</td>
|
||||
<td>auth.admin.failed_login_burst</td>
|
||||
<td>today 12:10</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="6" data-severity="medium" data-status="acked" data-group="storage" data-title="Cleanup skipped locked files" data-description="Cleanup job encountered locked files and skipped them." data-code="342" data-trace="cleanup.skip.locked_files" data-time="today 10:22" data-metadata='{"count":3,"connector":"local-main"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Cleanup skipped locked files</td>
|
||||
<td><span class="alerts-pill medium">medium</span></td>
|
||||
<td><span class="alerts-pill acked">acked</span></td>
|
||||
<td>342</td>
|
||||
<td>cleanup.skip.locked_files</td>
|
||||
<td>today 10:22</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="5" data-severity="low" data-status="closed" data-group="uploads" data-title="Archive completed with warnings" data-description="ZIP archive completed but excluded one unreadable temporary file." data-code="145" data-trace="archive.complete.with_warning" data-time="today 09:02" data-metadata='{"box":"bx_3901","skipped":1}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Archive completed with warnings</td>
|
||||
<td><span class="alerts-pill low">low</span></td>
|
||||
<td><span class="alerts-pill closed">closed</span></td>
|
||||
<td>145</td>
|
||||
<td>archive.complete.with_warning</td>
|
||||
<td>today 09:02</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="4" data-severity="medium" data-status="open" data-group="uploads" data-title="Upload session expired mid-transfer" data-description="A long-running upload lost session validity before final commit." data-code="156" data-trace="upload.session.expired_mid_transfer" data-time="yesterday" data-metadata='{"user":"teo","partial_bytes":"1.2 GB"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Upload session expired mid-transfer</td>
|
||||
<td><span class="alerts-pill medium">medium</span></td>
|
||||
<td><span class="alerts-pill open">open</span></td>
|
||||
<td>156</td>
|
||||
<td>upload.session.expired_mid_transfer</td>
|
||||
<td>yesterday</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="3" data-severity="low" data-status="closed" data-group="thumbnails" data-title="Thumbnail worker restarted" data-description="Thumbnail worker restarted after a normal watchdog recycle." data-code="602" data-trace="thumbnail.worker.restarted" data-time="yesterday" data-metadata='{"worker":"thumb-1","reason":"watchdog"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Thumbnail worker restarted</td>
|
||||
<td><span class="alerts-pill low">low</span></td>
|
||||
<td><span class="alerts-pill closed">closed</span></td>
|
||||
<td>602</td>
|
||||
<td>thumbnail.worker.restarted</td>
|
||||
<td>yesterday</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="2" data-severity="medium" data-status="acked" data-group="auth" data-title="User invited without email delivery confirmation" data-description="Invite creation succeeded but email delivery confirmation was not returned." data-code="224" data-trace="auth.invite.delivery_unknown" data-time="2 days ago" data-metadata='{"user":"reo","provider":"smtp-primary"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>User invited without email delivery confirmation</td>
|
||||
<td><span class="alerts-pill medium">medium</span></td>
|
||||
<td><span class="alerts-pill acked">acked</span></td>
|
||||
<td>224</td>
|
||||
<td>auth.invite.delivery_unknown</td>
|
||||
<td>2 days ago</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
<tr data-id="1" data-severity="low" data-status="closed" data-group="storage" data-title="Secondary connector caught up" data-description="Delayed sync on a secondary storage connector completed successfully." data-code="329" data-trace="storage.secondary.sync_recovered" data-time="2 days ago" data-metadata='{"connector":"bucket-archive","lag":"0"}'>
|
||||
<td><input type="checkbox" class="row-check"></td>
|
||||
<td>Secondary connector caught up</td>
|
||||
<td><span class="alerts-pill low">low</span></td>
|
||||
<td><span class="alerts-pill closed">closed</span></td>
|
||||
<td>329</td>
|
||||
<td>storage.secondary.sync_recovered</td>
|
||||
<td>2 days ago</td>
|
||||
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody id="alerts-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,10 +186,11 @@
|
||||
<div class="alerts-action-stack">
|
||||
<button class="win98-button alerts-action-button" type="button" data-command="ack">Acknowledge selected</button>
|
||||
<button class="win98-button alerts-action-button" type="button" data-command="close">Close selected</button>
|
||||
<button class="win98-button alerts-action-button" type="button" data-command="delete">Delete selected</button>
|
||||
<button class="win98-button alerts-action-button" type="button" data-command="refresh">Refresh alerts</button>
|
||||
</div>
|
||||
<div class="alerts-mini-note">
|
||||
CURRENTLY_MOCKED_LEAVE_AS_IS: alerts use a lightweight lifecycle for now: open, acknowledged, closed.
|
||||
Alerts persist until deleted. Acknowledge and close update state; delete removes permanently.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -301,11 +201,12 @@
|
||||
<div class="alerts-footerbar">
|
||||
<div class="alerts-footer-left">
|
||||
<span class="alerts-status-pill" id="selected-count">Selected: 0</span>
|
||||
<span class="alerts-status-pill">10 mocked alerts</span>
|
||||
<span class="alerts-status-pill" id="alerts-total-pill">{{ len .Alerts }} alerts</span>
|
||||
</div>
|
||||
<div class="alerts-footer-right">
|
||||
<button class="win98-button alerts-footer-button" type="button" data-command="ack">Acknowledge</button>
|
||||
<button class="win98-button alerts-footer-button" type="button" data-command="close">Close</button>
|
||||
<button class="win98-button alerts-footer-button" type="button" data-command="delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,6 +215,7 @@
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script id="alerts-data" type="application/json">{{ toJSON .Alerts }}</script>
|
||||
<script src="/static/js/warpbox-ui.js"></script>
|
||||
<script src="/static/js/admin/alerts.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<a class="admin-taskbar-button{{ if eq .ActivePage "dashboard" }} is-active{{ end }}" href="/admin/dashboard">Dashboard</a>
|
||||
<a class="admin-taskbar-button{{ if eq .ActivePage "alerts" }} is-active{{ end }}" href="/admin/alerts">Alerts</a>
|
||||
<a class="admin-taskbar-button{{ if eq .ActivePage "boxes" }} is-active{{ end }}" href="/admin/boxes">Boxes</a>
|
||||
<a class="admin-taskbar-button{{ if eq .ActivePage "activity" }} is-active{{ end }}" href="/admin/activity">Activity</a>
|
||||
<a class="admin-taskbar-button{{ if eq .ActivePage "users" }} is-active{{ end }}" href="/admin/users">Users</a>
|
||||
<a class="admin-taskbar-button{{ if eq .ActivePage "security" }} is-active{{ end }}" href="/admin/security">Security</a>
|
||||
<a class="admin-taskbar-button{{ if eq .ActivePage "settings" }} is-active{{ end }}" href="/admin/settings">Settings</a>
|
||||
</nav>
|
||||
<div class="admin-taskbar-session" aria-label="Admin session summary">
|
||||
|
||||
138
templates/admin/security.html
Normal file
138
templates/admin/security.html
Normal file
@@ -0,0 +1,138 @@
|
||||
{{ define "admin/security.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WarpBox Admin Security</title>
|
||||
<link rel="icon" type="image/png" href="/static/WarpBoxLogo.png">
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
<link rel="stylesheet" href="/static/css/window.css">
|
||||
<link rel="stylesheet" href="/static/css/components/buttons.css">
|
||||
<link rel="stylesheet" href="/static/css/components/toast.css">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
<link rel="stylesheet" href="/static/css/security.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-shell">
|
||||
<div class="admin-frame">
|
||||
{{ template "admin/header.html" . }}
|
||||
<div class="win98-window admin-workspace-window" role="main">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
||||
<h1>WarpBox Security</h1>
|
||||
</div>
|
||||
<div class="win98-window-controls" aria-hidden="true">
|
||||
<button class="win98-control" type="button">_</button>
|
||||
<button class="win98-control" type="button">□</button>
|
||||
<button class="win98-control" type="button">x</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="menu-bar" aria-label="Security toolbar">
|
||||
<div class="menu-item">
|
||||
<button class="menu-button" type="button" aria-expanded="false">Security</button>
|
||||
<div class="menu-popup">
|
||||
<button class="menu-action" type="button" data-command="ban-ip"><span>B</span><span>Ban IP now</span><span></span></button>
|
||||
<button class="menu-action" type="button" data-command="ban-until"><span>T</span><span>Set ban expiration</span><span></span></button>
|
||||
<button class="menu-action" type="button" data-command="unban-ip"><span>U</span><span>Unban selected IP</span><span></span></button>
|
||||
<button class="menu-action" type="button" data-command="refresh"><span>R</span><span>Refresh data</span><span>F5</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="admin-workspace-body security-page-body">
|
||||
<section class="security-grid">
|
||||
<section class="security-panel">
|
||||
<div class="security-panel-header"><strong>Manual controls</strong><span>basic first version</span></div>
|
||||
<div class="security-panel-body">
|
||||
<label class="security-field">IP address
|
||||
<input class="security-input" id="security-ip-input" type="text" placeholder="203.0.113.12">
|
||||
</label>
|
||||
<button class="win98-button security-button" type="button" data-command="ban-ip">Ban IP (temporary)</button>
|
||||
<label class="security-field">Ban expires (UTC)
|
||||
<input class="security-input" id="security-ban-until" type="datetime-local">
|
||||
</label>
|
||||
<button class="win98-button security-button" type="button" data-command="ban-until">Set ban expiration</button>
|
||||
<button class="win98-button security-button" type="button" data-command="unban-ip">Unban selected IP</button>
|
||||
<div class="security-note">Ban duration and auto-ban thresholds come from Settings -> Security.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="security-panel">
|
||||
<div class="security-panel-header"><strong>Recent alerts</strong><span>{{ len .Alerts }} total</span></div>
|
||||
<div class="security-panel-body">
|
||||
<ul class="security-list" id="security-alert-list"></ul>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="security-panel">
|
||||
<div class="security-panel-header"><strong>IP addresses</strong><span id="security-bans-count">{{ len .Bans }} active bans</span></div>
|
||||
<div class="security-panel-body security-ban-grid">
|
||||
<div class="security-table-wrap security-bans-wrap">
|
||||
<table class="security-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Status</th>
|
||||
<th>Ban expires (UTC)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="security-bans-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="security-ip-detail">
|
||||
<h3 id="security-detail-ip">No IP selected</h3>
|
||||
<ul>
|
||||
<li><strong>Risk:</strong> <span id="security-detail-risk">-</span></li>
|
||||
<li><strong>Threat:</strong> <span id="security-detail-threat">-</span></li>
|
||||
<li><strong>Geo:</strong> <span id="security-detail-geo">Placeholder (geoipfast later)</span></li>
|
||||
<li><strong>ASN:</strong> <span id="security-detail-asn">Placeholder</span></li>
|
||||
<li><strong>Ban until:</strong> <span id="security-detail-until">-</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="security-panel">
|
||||
<div class="security-panel-header"><strong>Recent security activity</strong><span>{{ len .Events }} rows</span></div>
|
||||
<div class="security-panel-body">
|
||||
<div class="security-table-wrap">
|
||||
<table class="security-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Kind</th>
|
||||
<th>Severity</th>
|
||||
<th>IP</th>
|
||||
<th>Path</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="security-activity-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="status-bar admin-dashboard-statusbar">
|
||||
<span id="security-status-left">Security controls active</span>
|
||||
<span id="security-status-middle">alerts + activity linked</span>
|
||||
<span id="security-status-right">admin only</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
<script id="security-events-data" type="application/json">{{ toJSON .Events }}</script>
|
||||
<script id="security-alerts-data" type="application/json">{{ toJSON .Alerts }}</script>
|
||||
<script id="security-bans-data" type="application/json">{{ toJSON .Bans }}</script>
|
||||
<script src="/static/js/warpbox-ui.js"></script>
|
||||
<script src="/static/js/admin/security.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user