159 lines
6.4 KiB
HTML
159 lines
6.4 KiB
HTML
{{template "base" .}}
|
|
{{define "content"}}
|
|
<div style="max-width:1400px">
|
|
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
|
<div>
|
|
<div class="page-title">Countries</div>
|
|
<div class="page-sub">Country-scope bans managed via cscli decisions</div>
|
|
</div>
|
|
{{if .CLIAvailable}}<button class="btn-primary" onclick="toggleAddForm()">Add Country Ban</button>{{end}}
|
|
</div>
|
|
|
|
{{if not .CLIAvailable}}
|
|
<div class="panel" style="margin-bottom:16px">
|
|
<div class="panel-body">
|
|
<div class="empty-text">cscli not available</div>
|
|
<div class="empty-sub">Country management requires the cscli binary. Set cscli_path in app_config.conf.</div>
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
|
|
<div id="add-form" class="panel" style="display:none;margin-bottom:16px">
|
|
<div class="panel-header">
|
|
<span class="panel-title">Add Country Ban</span>
|
|
<button class="btn-ghost-sm" onclick="toggleAddForm()">Cancel</button>
|
|
</div>
|
|
<div class="panel-body">
|
|
<form method="POST" action="/countries/add">
|
|
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
|
<div style="display:grid;grid-template-columns:1fr 160px 160px;gap:16px;margin-bottom:16px">
|
|
<div>
|
|
<label class="field-label">Country Codes (ISO 3166-1 alpha-2)</label>
|
|
<textarea class="field-input" name="countries" rows="3" placeholder="CN RU KP" required autofocus
|
|
style="resize:vertical;font-family:'JetBrains Mono',monospace;text-transform:uppercase"></textarea>
|
|
<div class="field-hint">One per line or comma-separated. e.g. CN, RU, US</div>
|
|
</div>
|
|
<div>
|
|
<label class="field-label">Decision Type</label>
|
|
<select class="field-input" name="type">
|
|
<option value="ban">ban</option>
|
|
<option value="captcha">captcha</option>
|
|
<option value="throttle">throttle</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="field-label">Duration</label>
|
|
<input class="field-input" type="text" name="duration" id="dur-input" placeholder="24h" value="24h">
|
|
<div class="field-hint">Units: s m h d w</div>
|
|
<label style="display:flex;align-items:center;gap:6px;margin-top:8px;font-size:12px;cursor:pointer">
|
|
<input type="checkbox" name="permanent" value="1" id="chk-perm" onchange="togglePermanent(this)">
|
|
Permanent (10 yr)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;justify-content:flex-end">
|
|
<button type="submit" class="btn-primary">Add Decision</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<form method="POST" action="/countries/delete" id="bulk-form">
|
|
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
|
<div class="panel-header">
|
|
<span class="panel-title">Active Country Bans — Page {{.Page}}{{if .HasNext}} (more available){{end}}</span>
|
|
<div style="display:flex;gap:8px;align-items:center">
|
|
<button type="button" class="btn-ghost-sm" onclick="toggleAll()">Select all</button>
|
|
<button type="submit" class="btn-danger-sm" data-confirm-fn="confirmBulkDelete">Delete selected</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{if .Decisions}}
|
|
<div style="overflow-x:auto">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:36px"><input type="checkbox" id="chk-all" onchange="selectAll(this)"></th>
|
|
<th>Country</th>
|
|
<th>Type</th>
|
|
<th>Origin</th>
|
|
<th>Duration</th>
|
|
<th>Expires</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Decisions}}
|
|
<tr>
|
|
<td><input type="checkbox" name="id" value="{{.ID}}" class="row-chk"></td>
|
|
<td style="font-size:14px">
|
|
{{countryFlag .Value}} <span style="font-family:'JetBrains Mono',monospace">{{.Value}}</span>
|
|
</td>
|
|
<td><span class="badge {{decisionBadgeClass .Type}}">{{.Type}}</span></td>
|
|
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</span></td>
|
|
<td style="font-family:'JetBrains Mono',monospace;font-size:12px">{{.Duration}}</td>
|
|
<td style="font-size:12px;color:var(--muted)">{{if .Until}}{{truncate .Until 16}}{{else}}{{.Duration}}{{end}}</td>
|
|
<td>
|
|
<form method="POST" action="/countries/delete" style="display:inline">
|
|
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
|
<input type="hidden" name="id" value="{{.ID}}">
|
|
<button type="submit" class="btn-danger-sm" data-confirm="Remove ban for {{.Value}}?">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<div class="empty-state">
|
|
<div class="empty-text">No country bans active</div>
|
|
<div class="empty-sub">No country-scope decisions are in effect</div>
|
|
</div>
|
|
{{end}}
|
|
</form>
|
|
|
|
{{if or (gt .Page 1) .HasNext}}
|
|
<div class="pagination">
|
|
{{if gt .Page 1}}
|
|
<a href="/countries?page={{dec .Page}}" class="btn-ghost-sm">Prev</a>
|
|
{{end}}
|
|
<span style="font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--muted)">Page {{.Page}}</span>
|
|
{{if .HasNext}}
|
|
<a href="/countries?page={{inc .Page}}" class="btn-ghost-sm">Next</a>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "scripts"}}
|
|
<script>
|
|
function togglePermanent(cb) {
|
|
var dur = document.getElementById('dur-input');
|
|
if (dur) { dur.disabled = cb.checked; dur.value = cb.checked ? '87600h' : '24h'; }
|
|
}
|
|
function toggleAddForm() {
|
|
var f = document.getElementById('add-form');
|
|
f.style.display = f.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
function selectAll(cb) {
|
|
document.querySelectorAll('.row-chk').forEach(function(c) { c.checked = cb.checked; });
|
|
}
|
|
function toggleAll() {
|
|
var cb = document.getElementById('chk-all');
|
|
cb.checked = !cb.checked;
|
|
selectAll(cb);
|
|
}
|
|
function confirmBulkDelete() {
|
|
var n = document.querySelectorAll('.row-chk:checked').length;
|
|
if (n === 0) { appModal.info('Select at least one entry.'); return null; }
|
|
return 'Remove ' + n + ' country ban(s)?';
|
|
}
|
|
</script>
|
|
{{end}}
|