Initial commit — SETEC LABS Manager (Setec_CDM)
Flask-based VPS management panel with SSH remote command execution. Includes E2E encrypted SSH tunnel (AES-256-GCM + Go agent), setup wizard, security hardening tools, DNS management, firewall configs, monitoring, backup, and .sec patch update system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
299
setec-web/templates/base.html
Normal file
299
setec-web/templates/base.html
Normal file
@@ -0,0 +1,299 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SETEC LABS - {% block title %}Manager{% endblock %}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #0a0a0a;
|
||||
color: #00ff41;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
a { color: #00ff41; text-decoration: none; }
|
||||
a:hover { color: #fff; }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background: #111;
|
||||
border-right: 1px solid #00ff41;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0; left: 0; bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sidebar-logo {
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #333;
|
||||
text-align: center;
|
||||
}
|
||||
.sidebar-logo img { width: 100%; max-width: 200px; height: auto; }
|
||||
.sidebar nav { padding: 10px 0; flex: 1; overflow-y: auto; }
|
||||
.sidebar nav a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.sidebar nav a:hover, .sidebar nav a.active {
|
||||
background: #1a2a1a;
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar nav a .icon { margin-right: 8px; }
|
||||
.sidebar-bottom {
|
||||
border-top: 1px solid #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-bottom a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
transition: background 0.2s;
|
||||
color: #00ff41;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sidebar-bottom a:hover, .sidebar-bottom a.active {
|
||||
background: #1a2a1a;
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar-bottom a .icon { margin-right: 8px; }
|
||||
.sidebar .ver {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main {
|
||||
margin-left: 220px;
|
||||
flex: 1;
|
||||
padding: 20px 30px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h2 { font-size: 14px; margin: 15px 0 8px; color: #88ff88; }
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
color: #88ff88;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #222;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
background: #1a2a1a;
|
||||
color: #00ff41;
|
||||
border: 1px solid #00ff41;
|
||||
padding: 6px 14px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
margin: 3px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn:hover { background: #00ff41; color: #000; }
|
||||
.btn-danger { border-color: #ff4444; color: #ff4444; }
|
||||
.btn-danger:hover { background: #ff4444; color: #000; }
|
||||
.btn-warn { border-color: #ffaa00; color: #ffaa00; }
|
||||
.btn-warn:hover { background: #ffaa00; color: #000; }
|
||||
|
||||
/* Output */
|
||||
.output {
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
color: #00ff41;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.output .err { color: #ff4444; }
|
||||
.output .info { color: #888; }
|
||||
|
||||
/* Forms */
|
||||
input, select, textarea {
|
||||
background: #000;
|
||||
color: #00ff41;
|
||||
border: 1px solid #333;
|
||||
padding: 6px 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
margin: 3px;
|
||||
}
|
||||
input:focus, textarea:focus { border-color: #00ff41; outline: none; }
|
||||
label { font-size: 12px; color: #888; display: block; margin: 5px 3px 2px; }
|
||||
|
||||
/* Table */
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th { text-align: left; color: #888; border-bottom: 1px solid #333; padding: 6px; }
|
||||
td { padding: 6px; border-bottom: 1px solid #1a1a1a; }
|
||||
tr:hover { background: #1a1a1a; }
|
||||
|
||||
/* Grid */
|
||||
.grid { display: grid; gap: 15px; }
|
||||
.grid-2 { grid-template-columns: 1fr 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||
|
||||
/* Status indicators */
|
||||
.status-ok { color: #00ff41; }
|
||||
.status-err { color: #ff4444; }
|
||||
.status-warn { color: #ffaa00; }
|
||||
|
||||
/* Loading */
|
||||
.loading::after {
|
||||
content: '...';
|
||||
animation: dots 1s steps(3) infinite;
|
||||
}
|
||||
@keyframes dots {
|
||||
0% { content: '.'; }
|
||||
33% { content: '..'; }
|
||||
66% { content: '...'; }
|
||||
}
|
||||
.toolbar { margin-bottom: 15px; display: flex; gap: 5px; flex-wrap: wrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<img src="{{ url_for('static', filename='setec_labs_logo.svg') }}" alt="SETEC LABS">
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/" class="{% if request.endpoint == 'dashboard' %}active{% endif %}">
|
||||
<span class="icon">[~]</span> Dashboard
|
||||
</a>
|
||||
<a href="/docker" class="{% if request.endpoint == 'docker_page' %}active{% endif %}">
|
||||
<span class="icon">[#]</span> Docker
|
||||
</a>
|
||||
<a href="/dns" class="{% if request.endpoint == 'dns_page' %}active{% endif %}">
|
||||
<span class="icon">[@]</span> DNS
|
||||
</a>
|
||||
<a href="/nginx" class="{% if request.endpoint == 'nginx_page' %}active{% endif %}">
|
||||
<span class="icon">[>]</span> Nginx
|
||||
</a>
|
||||
<a href="/smtp" class="{% if request.endpoint == 'smtp_page' %}active{% endif %}">
|
||||
<span class="icon">[*]</span> SMTP
|
||||
</a>
|
||||
<a href="/firewall" class="{% if request.endpoint == 'firewall_page' %}active{% endif %}">
|
||||
<span class="icon">[|]</span> Firewall
|
||||
</a>
|
||||
<a href="/fail2ban" class="{% if request.endpoint == 'fail2ban_page' %}active{% endif %}">
|
||||
<span class="icon">[!]</span> Fail2Ban
|
||||
</a>
|
||||
<a href="/frontpage" class="{% if request.endpoint == 'frontpage_page' %}active{% endif %}">
|
||||
<span class="icon">[<]</span> Front Page
|
||||
</a>
|
||||
<a href="/security" class="{% if request.endpoint == 'security_page' %}active{% endif %}">
|
||||
<span class="icon">[&]</span> Security
|
||||
</a>
|
||||
<a href="/detect" class="{% if request.endpoint == 'detect_page' %}active{% endif %}">
|
||||
<span class="icon">[?]</span> Detect
|
||||
</a>
|
||||
<a href="/configs" class="{% if request.endpoint == 'configs_page' %}active{% endif %}">
|
||||
<span class="icon">[=]</span> Configs
|
||||
</a>
|
||||
<a href="/files" class="{% if request.endpoint == 'files_page' %}active{% endif %}">
|
||||
<span class="icon">[/]</span> Files
|
||||
</a>
|
||||
<a href="/terminal" class="{% if request.endpoint == 'terminal_page' %}active{% endif %}">
|
||||
<span class="icon">[$]</span> Terminal
|
||||
</a>
|
||||
<a href="/settings" class="{% if request.endpoint == 'settings_page' %}active{% endif %}">
|
||||
<span class="icon">[%]</span> Settings
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-bottom">
|
||||
<a href="/docs" class="{% if request.endpoint == 'docs_page' %}active{% endif %}">
|
||||
<span class="icon">[^]</span> Docs
|
||||
</a>
|
||||
<a href="/wizard" class="{% if request.endpoint == 'wizard_page' %}active{% endif %}">
|
||||
<span class="icon">[+]</span> Setup Wizard
|
||||
</a>
|
||||
<div class="ver">setec-mgr v2.0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script>
|
||||
async function api(url, opts = {}) {
|
||||
const el = document.getElementById('output');
|
||||
if (el) el.innerHTML = '<span class="info">Loading...</span>';
|
||||
try {
|
||||
const r = await fetch(url, opts);
|
||||
const j = await r.json();
|
||||
return j;
|
||||
} catch (e) {
|
||||
if (el) el.innerHTML = '<span class="err">Error: ' + e.message + '</span>';
|
||||
return {ok: false, error: e.message};
|
||||
}
|
||||
}
|
||||
|
||||
async function apiGet(url) { return api(url); }
|
||||
|
||||
async function apiPost(url, body = {}) {
|
||||
return api(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
async function apiDelete(url, body = {}) {
|
||||
return api(url, {
|
||||
method: 'DELETE',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
function showResult(res, elId = 'output') {
|
||||
const el = document.getElementById(elId);
|
||||
if (!el) return;
|
||||
if (!res.ok) {
|
||||
el.innerHTML = '<span class="err">ERROR: ' + (res.error || 'Unknown error') + '</span>';
|
||||
return;
|
||||
}
|
||||
const d = res.data;
|
||||
if (typeof d === 'string') {
|
||||
el.textContent = d;
|
||||
} else if (d && d.stdout !== undefined) {
|
||||
let html = '';
|
||||
if (d.stdout) html += d.stdout;
|
||||
if (d.stderr) html += '<span class="err">' + escHtml(d.stderr) + '</span>';
|
||||
if (d.exit_code && d.exit_code !== 0) html += '\n<span class="err">[exit code: ' + d.exit_code + ']</span>';
|
||||
el.innerHTML = html || '<span class="info">(no output)</span>';
|
||||
} else {
|
||||
el.textContent = JSON.stringify(d, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
222
setec-web/templates/configs.html
Normal file
222
setec-web/templates/configs.html
Normal file
@@ -0,0 +1,222 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Config Editor{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[=] Config Editor</h1>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">Quick Access</div>
|
||||
<h2>Nginx</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/nginx/nginx.conf')">nginx.conf</button>
|
||||
<button class="btn" onclick="listDir('/etc/nginx/sites-available/')">sites-available/</button>
|
||||
<button class="btn" onclick="listDir('/etc/nginx/conf.d/')">conf.d/</button>
|
||||
<button class="btn" onclick="listDir('/etc/nginx/snippets/')">snippets/</button>
|
||||
</div>
|
||||
<h2>GitLab</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/gitlab/gitlab.rb')">gitlab.rb</button>
|
||||
</div>
|
||||
<h2>Gitea</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/gitea/app.ini')">app.ini (etc)</button>
|
||||
<button class="btn" onclick="loadConfig('/var/lib/gitea/custom/conf/app.ini')">app.ini (custom)</button>
|
||||
</div>
|
||||
<h2>SSH</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/ssh/sshd_config')">sshd_config</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/ssh/ssh_config')">ssh_config</button>
|
||||
</div>
|
||||
<h2>Mail</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/postfix/main.cf')">postfix main.cf</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/postfix/master.cf')">postfix master.cf</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/opendkim.conf')">opendkim.conf</button>
|
||||
</div>
|
||||
<h2>Security</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/fail2ban/jail.local')">fail2ban jail</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/ufw/ufw.conf')">ufw.conf</button>
|
||||
</div>
|
||||
<h2>Docker</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/docker/daemon.json')">daemon.json</button>
|
||||
<button class="btn" onclick="loadConfig('/opt/seteclabs/docker-compose.yml')">docker-compose.yml</button>
|
||||
</div>
|
||||
<h2>System</h2>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadConfig('/etc/hosts')">hosts</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/hostname')">hostname</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/resolv.conf')">resolv.conf</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/fstab')">fstab</button>
|
||||
<button class="btn" onclick="loadConfig('/etc/crontab')">crontab</button>
|
||||
</div>
|
||||
<h2>Custom Path</h2>
|
||||
<div class="toolbar">
|
||||
<input type="text" id="custom-path" placeholder="/etc/some/config.conf" style="width:300px">
|
||||
<button class="btn" onclick="loadConfig(document.getElementById('custom-path').value)">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Directory Browser</div>
|
||||
<div class="output" id="dir-list" style="max-height:300px"><span class="info">Click a directory button above</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
Editor: <span id="current-file" style="color:#ffaa00">none</span>
|
||||
</div>
|
||||
<textarea id="config-editor" rows="30" style="width:100%;resize:vertical;tab-size:4" spellcheck="false"></textarea>
|
||||
<div class="toolbar" style="margin-top:5px">
|
||||
<button class="btn" onclick="saveConfig()">Save</button>
|
||||
<button class="btn" onclick="testConfig()">Test Nginx</button>
|
||||
<button class="btn" onclick="showDiff()">Diff vs Backup</button>
|
||||
<button class="btn" onclick="showBackups()">List Backups</button>
|
||||
<button class="btn btn-warn" onclick="reloadSvc()">Reload Service</button>
|
||||
</div>
|
||||
<div style="margin-top:5px">
|
||||
<label>Reload service after save:</label>
|
||||
<select id="reload-svc">
|
||||
<option value="">-- none --</option>
|
||||
<option value="nginx">nginx</option>
|
||||
<option value="sshd">sshd</option>
|
||||
<option value="postfix">postfix</option>
|
||||
<option value="opendkim">opendkim</option>
|
||||
<option value="fail2ban">fail2ban</option>
|
||||
<option value="docker">docker</option>
|
||||
<option value="redis">redis</option>
|
||||
<option value="postgresql">postgresql</option>
|
||||
<option value="mysql">mysql</option>
|
||||
<option value="dovecot">dovecot</option>
|
||||
<option value="grafana-server">grafana</option>
|
||||
<option value="prometheus">prometheus</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output</div>
|
||||
<div class="output" id="output"><span class="info">Ready. Click a config file to edit.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentFile = '';
|
||||
|
||||
// Check URL params for ?file=
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('file')) {
|
||||
loadConfig(params.get('file'));
|
||||
}
|
||||
|
||||
async function loadConfig(path) {
|
||||
if (!path) return;
|
||||
currentFile = path;
|
||||
document.getElementById('current-file').textContent = path;
|
||||
document.getElementById('config-editor').value = 'Loading...';
|
||||
const res = await apiGet('/api/configs/read?path=' + encodeURIComponent(path));
|
||||
if (res.ok) {
|
||||
document.getElementById('config-editor').value = res.data.stdout || '';
|
||||
document.getElementById('output').innerHTML = '<span class="info">Loaded ' + escHtml(path) + ' (' + (res.data.stdout||'').split('\n').length + ' lines)</span>';
|
||||
} else {
|
||||
document.getElementById('config-editor').value = '';
|
||||
document.getElementById('output').innerHTML = '<span class="err">' + escHtml(res.error || 'Failed to read file') + '</span>';
|
||||
}
|
||||
// Auto-detect reload service
|
||||
if (path.includes('nginx')) document.getElementById('reload-svc').value = 'nginx';
|
||||
else if (path.includes('postfix')) document.getElementById('reload-svc').value = 'postfix';
|
||||
else if (path.includes('sshd') || path.includes('/ssh/')) document.getElementById('reload-svc').value = 'sshd';
|
||||
else if (path.includes('opendkim')) document.getElementById('reload-svc').value = 'opendkim';
|
||||
else if (path.includes('fail2ban')) document.getElementById('reload-svc').value = 'fail2ban';
|
||||
else if (path.includes('docker')) document.getElementById('reload-svc').value = 'docker';
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
if (!currentFile) { alert('No file loaded'); return; }
|
||||
if (!confirm('Save changes to ' + currentFile + '?\nA backup will be created automatically.')) return;
|
||||
const content = document.getElementById('config-editor').value;
|
||||
const res = await apiPost('/api/configs/write', {path: currentFile, content: content});
|
||||
if (res.ok) {
|
||||
document.getElementById('output').innerHTML = '<span class="status-ok">Saved ' + escHtml(currentFile) + ' (backup created)</span>';
|
||||
// Auto-reload service if selected
|
||||
const svc = document.getElementById('reload-svc').value;
|
||||
if (svc) {
|
||||
const r2 = await apiPost('/api/configs/reload-service', {service: svc});
|
||||
showResult(r2);
|
||||
}
|
||||
} else {
|
||||
showResult(res);
|
||||
}
|
||||
}
|
||||
|
||||
async function testConfig() {
|
||||
const res = await apiPost('/api/configs/test-nginx');
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function showDiff() {
|
||||
if (!currentFile) return;
|
||||
const res = await apiPost('/api/configs/diff', {path: currentFile});
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function showBackups() {
|
||||
if (!currentFile) return;
|
||||
const res = await apiGet('/api/configs/backups?path=' + encodeURIComponent(currentFile));
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function reloadSvc() {
|
||||
const svc = document.getElementById('reload-svc').value;
|
||||
if (!svc) { alert('Select a service to reload'); return; }
|
||||
const res = await apiPost('/api/configs/reload-service', {service: svc});
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function listDir(path) {
|
||||
const res = await apiGet('/api/files/list?path=' + encodeURIComponent(path));
|
||||
const el = document.getElementById('dir-list');
|
||||
if (!res.ok) { el.innerHTML = '<span class="err">' + res.error + '</span>'; return; }
|
||||
|
||||
const lines = (res.data.stdout || '').trim().split('\n');
|
||||
let html = '';
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 9) continue;
|
||||
const fname = parts.slice(8).join(' ');
|
||||
if (fname === '.' || fname === '..') continue;
|
||||
const isDir = line.startsWith('d');
|
||||
const fullPath = path.replace(/\/$/, '') + '/' + fname;
|
||||
if (isDir) {
|
||||
html += `<a href="#" onclick="listDir('${fullPath}/');return false" style="color:#ffaa00">${fname}/</a>\n`;
|
||||
} else {
|
||||
html += `<a href="#" onclick="loadConfig('${fullPath}');return false">${fname}</a>\n`;
|
||||
}
|
||||
}
|
||||
el.innerHTML = html || '<span class="info">(empty)</span>';
|
||||
}
|
||||
|
||||
// Handle tab key in editor
|
||||
document.getElementById('config-editor').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 4;
|
||||
}
|
||||
// Ctrl+S to save
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
54
setec-web/templates/dashboard.html
Normal file
54
setec-web/templates/dashboard.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[~] Dashboard</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadStatus()">Refresh Status</button>
|
||||
<button class="btn" onclick="loadDomains()">Check Domains</button>
|
||||
<button class="btn" onclick="deploySite()">Deploy Site</button>
|
||||
<button class="btn" onclick="testSSH()">Test SSH</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Server Status</div>
|
||||
<div class="output" id="status-output"><span class="info">Click "Refresh Status" to load</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Domain Status</div>
|
||||
<div class="output" id="domain-output"><span class="info">Click "Check Domains" to load</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Quick Actions</div>
|
||||
<div class="output" id="output"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function loadStatus() {
|
||||
const res = await apiGet('/api/status');
|
||||
showResult(res, 'status-output');
|
||||
}
|
||||
async function loadDomains() {
|
||||
const res = await apiGet('/api/status/domains');
|
||||
showResult(res, 'domain-output');
|
||||
}
|
||||
async function deploySite() {
|
||||
if (!confirm('Deploy site/ to VPS?')) return;
|
||||
document.getElementById('output').innerHTML = '<span class="info">Deploying...</span>';
|
||||
const res = await apiPost('/api/deploy/site');
|
||||
showResult(res);
|
||||
}
|
||||
async function testSSH() {
|
||||
const res = await apiGet('/api/ssh/test');
|
||||
showResult(res);
|
||||
}
|
||||
// Auto-load on page load
|
||||
loadStatus();
|
||||
loadDomains();
|
||||
</script>
|
||||
{% endblock %}
|
||||
140
setec-web/templates/detect.html
Normal file
140
setec-web/templates/detect.html
Normal file
@@ -0,0 +1,140 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Service Detection{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[?] Service Detection</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="runScan()">Scan Server</button>
|
||||
<button class="btn" onclick="browseAll()">Browse All 200+ Services</button>
|
||||
</div>
|
||||
|
||||
<div id="scan-results"></div>
|
||||
|
||||
<div class="card" id="browse-panel" style="display:none">
|
||||
<div class="card-title">Service Database ({{count}} services)</div>
|
||||
<input type="text" id="svc-search" placeholder="Filter services..." style="width:100%;margin-bottom:10px" oninput="filterServices()">
|
||||
<div id="all-services" style="max-height:600px;overflow-y:auto"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output</div>
|
||||
<div class="output" id="output"><span class="info">Click "Scan Server" to detect installed services</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let allServices = {};
|
||||
|
||||
async function runScan() {
|
||||
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="card-title">Scanning...</div><div class="output"><span class="info">Checking processes, ports, packages, configs, docker, systemd...</span></div></div>';
|
||||
const res = await apiGet('/api/detect/scan');
|
||||
if (!res.ok) {
|
||||
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="err">'+res.error+'</span></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const services = res.data;
|
||||
if (!services.length) {
|
||||
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="info">No services detected (or SSH failed)</span></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by category
|
||||
const groups = {};
|
||||
services.forEach(s => {
|
||||
const cat = s.category;
|
||||
if (!groups[cat]) groups[cat] = [];
|
||||
groups[cat].push(s);
|
||||
});
|
||||
|
||||
let html = '<div class="card"><div class="card-title">Detected Services (' + services.length + ' found)</div></div>';
|
||||
|
||||
for (const [cat, svcs] of Object.entries(groups)) {
|
||||
html += '<div class="card"><div class="card-title">' + escHtml(cat) + ' (' + svcs.length + ')</div>';
|
||||
html += '<table><thead><tr><th>Service</th><th>Confidence</th><th>Evidence</th><th>Configs</th><th>Actions</th></tr></thead><tbody>';
|
||||
for (const s of svcs) {
|
||||
const conf = s.score >= 5 ? 'status-ok' : s.score >= 3 ? 'status-warn' : 'status-err';
|
||||
const confLabel = s.score >= 5 ? 'HIGH' : s.score >= 3 ? 'MED' : 'LOW';
|
||||
const configLinks = s.configs.filter(c => c && !c.endsWith('/')).map(c =>
|
||||
`<a href="#" onclick="editConfig('${c}');return false" style="font-size:11px">${c.split('/').pop()}</a>`
|
||||
).join(', ');
|
||||
const configDirs = s.configs.filter(c => c && c.endsWith('/')).map(c =>
|
||||
`<a href="#" onclick="browseConfig('${c}');return false" style="font-size:11px;color:#888">${c}</a>`
|
||||
).join(', ');
|
||||
|
||||
html += '<tr>';
|
||||
html += '<td><strong>' + escHtml(s.name) + '</strong></td>';
|
||||
html += '<td class="' + conf + '">' + confLabel + ' (' + s.score + ')</td>';
|
||||
html += '<td style="font-size:11px;color:#888">' + s.evidence.map(escHtml).join(', ') + '</td>';
|
||||
html += '<td>' + configLinks + (configDirs ? '<br>' + configDirs : '') + '</td>';
|
||||
html += '<td>';
|
||||
if (s.ports.length) html += '<span style="color:#555">ports: ' + s.ports.join(',') + '</span> ';
|
||||
html += '</td>';
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
document.getElementById('scan-results').innerHTML = html;
|
||||
}
|
||||
|
||||
function editConfig(path) {
|
||||
window.location.href = '/configs?file=' + encodeURIComponent(path);
|
||||
}
|
||||
|
||||
function browseConfig(path) {
|
||||
window.location.href = '/files?path=' + encodeURIComponent(path);
|
||||
}
|
||||
|
||||
async function browseAll() {
|
||||
const panel = document.getElementById('browse-panel');
|
||||
panel.style.display = '';
|
||||
if (Object.keys(allServices).length) return;
|
||||
|
||||
const res = await apiGet('/api/detect/all-services');
|
||||
if (!res.ok) return;
|
||||
allServices = res.data;
|
||||
renderAllServices(allServices);
|
||||
}
|
||||
|
||||
function renderAllServices(data) {
|
||||
let html = '';
|
||||
let total = 0;
|
||||
for (const [cat, svcs] of Object.entries(data)) {
|
||||
total += svcs.length;
|
||||
html += '<h2 style="margin-top:10px">' + escHtml(cat) + ' (' + svcs.length + ')</h2>';
|
||||
html += '<table><thead><tr><th>Service</th><th>Ports</th><th>Packages</th><th>Config Files</th></tr></thead><tbody>';
|
||||
for (const s of svcs) {
|
||||
html += '<tr>';
|
||||
html += '<td>' + escHtml(s.name) + '</td>';
|
||||
html += '<td style="color:#888">' + (s.ports.length ? s.ports.join(', ') : '-') + '</td>';
|
||||
html += '<td style="font-size:11px;color:#888">' + (s.packages.length ? s.packages.join(', ') : '-') + '</td>';
|
||||
html += '<td style="font-size:11px">' + (s.configs.length ? s.configs.map(c =>
|
||||
'<a href="#" onclick="editConfig(\'' + c + '\');return false">' + c + '</a>'
|
||||
).join('<br>') : '-') + '</td>';
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
document.getElementById('all-services').innerHTML = html;
|
||||
document.querySelector('#browse-panel .card-title').textContent = 'Service Database (' + total + ' services)';
|
||||
}
|
||||
|
||||
function filterServices() {
|
||||
const q = document.getElementById('svc-search').value.toLowerCase();
|
||||
if (!q) { renderAllServices(allServices); return; }
|
||||
const filtered = {};
|
||||
for (const [cat, svcs] of Object.entries(allServices)) {
|
||||
const matches = svcs.filter(s =>
|
||||
s.name.toLowerCase().includes(q) ||
|
||||
cat.toLowerCase().includes(q) ||
|
||||
s.packages.some(p => p.includes(q)) ||
|
||||
s.configs.some(c => c.includes(q))
|
||||
);
|
||||
if (matches.length) filtered[cat] = matches;
|
||||
}
|
||||
renderAllServices(filtered);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
101
setec-web/templates/dns.html
Normal file
101
setec-web/templates/dns.html
Normal file
@@ -0,0 +1,101 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}DNS{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[@] DNS Management</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadRecords()">Refresh Records</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">DNS Records (Hostinger)</div>
|
||||
<div id="dns-records" class="output" style="max-height:600px"><span class="info">Loading...</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">Add Record</div>
|
||||
<label>Type</label>
|
||||
<select id="dns-type">
|
||||
<option value="A">A</option>
|
||||
<option value="TXT">TXT</option>
|
||||
</select>
|
||||
<label>Name (subdomain or @ for root)</label>
|
||||
<input type="text" id="dns-name" placeholder="e.g. app" style="width:100%">
|
||||
<label>Value</label>
|
||||
<input type="text" id="dns-value" placeholder="e.g. 31.220.20.55" style="width:100%">
|
||||
<br><br>
|
||||
<button class="btn" onclick="addRecord()">Add Record</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Delete Record</div>
|
||||
<label>Record ID</label>
|
||||
<input type="text" id="dns-del-id" placeholder="record ID from list" style="width:100%">
|
||||
<br><br>
|
||||
<button class="btn btn-danger" onclick="deleteRecord()">Delete Record</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output</div>
|
||||
<div class="output" id="output"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function loadRecords() {
|
||||
const el = document.getElementById('dns-records');
|
||||
el.innerHTML = '<span class="info">Loading...</span>';
|
||||
const res = await apiGet('/api/dns/records');
|
||||
if (!res.ok) { el.innerHTML = '<span class="err">'+res.error+'</span>'; return; }
|
||||
const records = Array.isArray(res.data) ? res.data : (res.data.records || [res.data]);
|
||||
let html = '';
|
||||
for (const r of records) {
|
||||
if (Array.isArray(r)) {
|
||||
for (const rec of r) {
|
||||
html += formatRecord(rec);
|
||||
}
|
||||
} else {
|
||||
html += formatRecord(r);
|
||||
}
|
||||
}
|
||||
el.innerHTML = html || '<span class="info">No records found</span>';
|
||||
}
|
||||
|
||||
function formatRecord(r) {
|
||||
const id = r.id || r.record_id || '?';
|
||||
const type = r.type || '?';
|
||||
const name = r.name || r.host || '?';
|
||||
const val = r.content || r.value || r.address || '?';
|
||||
const ttl = r.ttl || '';
|
||||
return `<div style="margin-bottom:4px;border-bottom:1px solid #222;padding-bottom:4px">` +
|
||||
`<span style="color:#888">[${id}]</span> ` +
|
||||
`<span style="color:#ffaa00">${type}</span> ` +
|
||||
`${name} → ${val} ` +
|
||||
`<span style="color:#555">TTL:${ttl}</span></div>`;
|
||||
}
|
||||
|
||||
async function addRecord() {
|
||||
const type = document.getElementById('dns-type').value;
|
||||
const name = document.getElementById('dns-name').value;
|
||||
const value = document.getElementById('dns-value').value;
|
||||
if (!name || !value) { alert('Fill in name and value'); return; }
|
||||
const res = await apiPost('/api/dns/add', {type, name, value});
|
||||
showResult(res);
|
||||
loadRecords();
|
||||
}
|
||||
|
||||
async function deleteRecord() {
|
||||
const id = document.getElementById('dns-del-id').value;
|
||||
if (!id) return;
|
||||
if (!confirm('Delete record '+id+'?')) return;
|
||||
const res = await apiDelete('/api/dns/delete/'+id);
|
||||
showResult(res);
|
||||
loadRecords();
|
||||
}
|
||||
|
||||
loadRecords();
|
||||
</script>
|
||||
{% endblock %}
|
||||
334
setec-web/templates/docker.html
Normal file
334
setec-web/templates/docker.html
Normal file
@@ -0,0 +1,334 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Docker{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[#] Docker Management</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadContainers()">Refresh</button>
|
||||
<button class="btn" onclick="composeAction('up')">Compose Up</button>
|
||||
<button class="btn btn-warn" onclick="composeAction('down')">Compose Down</button>
|
||||
<button class="btn" onclick="composeAction('pull')">Compose Pull</button>
|
||||
<button class="btn" onclick="showTab('containers')">Containers</button>
|
||||
<button class="btn" onclick="showTab('store')">App Store</button>
|
||||
<button class="btn" onclick="showTab('install')">Install from Git/URL</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Containers -->
|
||||
<div id="tab-containers">
|
||||
<div class="card">
|
||||
<div class="card-title">Running Containers</div>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Image</th><th>Status</th><th>Ports / Web UI</th><th>Actions</th></tr></thead>
|
||||
<tbody id="container-list"><tr><td colspan="5" class="info">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Container Logs</div>
|
||||
<div class="toolbar">
|
||||
<input type="text" id="log-container" placeholder="container name" style="width:200px">
|
||||
<input type="number" id="log-lines" value="50" style="width:80px">
|
||||
<button class="btn" onclick="loadLogs()">View Logs</button>
|
||||
</div>
|
||||
<div class="output" id="output"><span class="info">Select a container to view logs</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: App Store -->
|
||||
<div id="tab-store" style="display:none">
|
||||
<div class="card">
|
||||
<div class="card-title">Docker App Store</div>
|
||||
<div class="toolbar">
|
||||
<input type="text" id="store-search" placeholder="Search apps..." style="width:300px" oninput="filterStore()">
|
||||
<select id="store-cat" onchange="filterStore()">
|
||||
<option value="">All Categories</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="store-grid" class="grid grid-2" style="margin-top:10px"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Install Output</div>
|
||||
<div class="output" id="store-output"><span class="info">Select an app to install</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Install from Git/URL -->
|
||||
<div id="tab-install" style="display:none">
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Install from GitHub / Git Repo</div>
|
||||
<label>Git Repository URL</label>
|
||||
<input type="text" id="git-repo" placeholder="https://github.com/user/repo.git" style="width:100%">
|
||||
<label>App Name (optional, auto-detected from URL)</label>
|
||||
<input type="text" id="git-name" placeholder="my-app" style="width:100%">
|
||||
<br><br>
|
||||
<button class="btn" onclick="installGit()">Clone & Deploy</button>
|
||||
<p style="font-size:11px;color:#555;margin-top:8px">
|
||||
Clones the repo to /opt/seteclabs/<name> and runs docker compose up if a compose file is found.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Install from URL (tar.gz, zip, binary)</div>
|
||||
<label>Download URL</label>
|
||||
<input type="text" id="url-url" placeholder="https://example.com/app.tar.gz" style="width:100%">
|
||||
<label>App Name</label>
|
||||
<input type="text" id="url-name" placeholder="my-app" style="width:100%">
|
||||
<br><br>
|
||||
<button class="btn" onclick="installUrl()">Download & Deploy</button>
|
||||
<p style="font-size:11px;color:#555;margin-top:8px">
|
||||
Downloads to /opt/seteclabs/<name>/, extracts if archive, runs docker compose if found.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Install from Docker Compose (paste YAML)</div>
|
||||
<label>App Name</label>
|
||||
<input type="text" id="compose-name" placeholder="my-app" style="width:300px">
|
||||
<label>Docker Compose YAML</label>
|
||||
<textarea id="compose-yaml" rows="12" style="width:100%;tab-size:2" placeholder="services:
|
||||
myapp:
|
||||
image: myimage:latest
|
||||
ports:
|
||||
- '8080:80'"></textarea>
|
||||
<br>
|
||||
<button class="btn" onclick="installCompose()">Deploy Compose</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Install Output</div>
|
||||
<div class="output" id="install-output"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const VPS_IP = '31.220.20.55';
|
||||
let storeData = [];
|
||||
|
||||
// Known container -> web UI port mappings
|
||||
const UI_MAP = {
|
||||
'gitea': {port: 3000, proto: 'https', host: 'repo.seteclabs.io', path: '/'},
|
||||
'gitlab': {port: 8080, proto: 'https', host: 'git.seteclabs.io', path: '/'},
|
||||
'listmonk': {port: 9000, proto: 'https', host: 'lists.seteclabs.io', path: '/'},
|
||||
'portainer': {port: 9443, proto: 'https', path: '/'},
|
||||
'uptime-kuma': {port: 3001, path: '/'},
|
||||
'grafana': {port: 3002, path: '/'},
|
||||
'netdata': {port: 19999, path: '/'},
|
||||
'nginx-proxy-manager': {port: 81, path: '/'},
|
||||
'dockge': {port: 5001, path: '/'},
|
||||
'nextcloud': {port: 8081, path: '/'},
|
||||
'filebrowser': {port: 8082, path: '/'},
|
||||
'vaultwarden': {port: 8083, path: '/'},
|
||||
'wikijs': {port: 3003, path: '/'},
|
||||
'n8n': {port: 5678, path: '/'},
|
||||
'nodered': {port: 1880, path: '/'},
|
||||
'privatebin': {port: 8090, path: '/'},
|
||||
'it-tools': {port: 8091, path: '/'},
|
||||
'cyberchef': {port: 8092, path: '/'},
|
||||
'jellyfin': {port: 8096, path: '/'},
|
||||
'homer': {port: 8094, path: '/'},
|
||||
'homarr': {port: 7575, path: '/'},
|
||||
'wg-easy': {port: 51821, path: '/'},
|
||||
'woodpecker': {port: 8000, path: '/'},
|
||||
'plausible': {port: 8088, path: '/'},
|
||||
'umami': {port: 8089, path: '/'},
|
||||
'gotify': {port: 8085, path: '/'},
|
||||
'ntfy': {port: 8086, path: '/'},
|
||||
'cockpit': {port: 9090, path: '/'},
|
||||
'minio': {port: 9011, path: '/'},
|
||||
'bookstack': {port: 6875, path: '/'},
|
||||
'stirling-pdf': {port: 8093, path: '/'},
|
||||
};
|
||||
|
||||
function getWebLink(name, ports) {
|
||||
// Check known map first
|
||||
const known = UI_MAP[name];
|
||||
if (known) {
|
||||
const host = known.host || VPS_IP;
|
||||
const proto = known.proto || 'http';
|
||||
const port = (proto === 'https' && (known.port === 443 || known.host)) ? '' : ':' + known.port;
|
||||
return `${proto}://${host}${port}${known.path}`;
|
||||
}
|
||||
// Try to extract from port mapping string
|
||||
if (ports) {
|
||||
const match = ports.match(/0\.0\.0\.0:(\d+)/);
|
||||
if (match) return `http://${VPS_IP}:${match[1]}`;
|
||||
const match2 = ports.match(/:::(\d+)/);
|
||||
if (match2) return `http://${VPS_IP}:${match2[1]}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
function showTab(tab) {
|
||||
document.getElementById('tab-containers').style.display = tab === 'containers' ? '' : 'none';
|
||||
document.getElementById('tab-store').style.display = tab === 'store' ? '' : 'none';
|
||||
document.getElementById('tab-install').style.display = tab === 'install' ? '' : 'none';
|
||||
if (tab === 'store' && !storeData.length) loadStore();
|
||||
}
|
||||
|
||||
// ── Containers ──
|
||||
async function loadContainers() {
|
||||
const res = await apiGet('/api/docker/list');
|
||||
const tbody = document.getElementById('container-list');
|
||||
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="5" class="err">'+res.error+'</td></tr>'; return; }
|
||||
const lines = res.data.stdout.trim().split('\n').filter(l => l);
|
||||
if (!lines.length) { tbody.innerHTML = '<tr><td colspan="5" class="info">No containers</td></tr>'; return; }
|
||||
tbody.innerHTML = lines.map(line => {
|
||||
const [id, name, image, status, ports] = line.split('|');
|
||||
const running = status && status.includes('Up');
|
||||
const webLink = getWebLink(name, ports);
|
||||
const portDisplay = webLink
|
||||
? `<a href="${webLink}" target="_blank" style="color:#00ff41">${webLink}</a>`
|
||||
: `<span style="color:#888;font-size:11px">${ports||'none'}</span>`;
|
||||
|
||||
return `<tr>
|
||||
<td><strong>${name||''}</strong></td>
|
||||
<td style="color:#888;font-size:11px">${image||''}</td>
|
||||
<td class="${running?'status-ok':'status-err'}">${status||''}</td>
|
||||
<td>${portDisplay}</td>
|
||||
<td>
|
||||
<button class="btn" onclick="dockerAction('restart','${name}')">restart</button>
|
||||
${running
|
||||
? `<button class="btn btn-warn" onclick="dockerAction('stop','${name}')">stop</button>`
|
||||
: `<button class="btn" onclick="dockerAction('start','${name}')">start</button>`}
|
||||
<button class="btn" onclick="viewLogs('${name}')">logs</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function dockerAction(action, name) {
|
||||
const res = await apiPost('/api/docker/'+action+'/'+name);
|
||||
showResult(res);
|
||||
setTimeout(loadContainers, 1000);
|
||||
}
|
||||
|
||||
async function composeAction(action) {
|
||||
document.getElementById('output').innerHTML = '<span class="info">Running compose '+action+'...</span>';
|
||||
const res = await apiPost('/api/docker/compose/'+action);
|
||||
showResult(res);
|
||||
loadContainers();
|
||||
}
|
||||
|
||||
function viewLogs(name) {
|
||||
showTab('containers');
|
||||
document.getElementById('log-container').value = name;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const name = document.getElementById('log-container').value;
|
||||
const lines = document.getElementById('log-lines').value;
|
||||
if (!name) return;
|
||||
const res = await apiGet('/api/docker/logs/'+name+'?lines='+lines);
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
// ── Store ──
|
||||
async function loadStore() {
|
||||
const res = await apiGet('/api/docker/store');
|
||||
if (!res.ok) return;
|
||||
storeData = res.data;
|
||||
|
||||
const catSelect = document.getElementById('store-cat');
|
||||
const cats = res.categories || [...new Set(storeData.map(a => a.cat))];
|
||||
cats.forEach(c => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c; opt.textContent = c;
|
||||
catSelect.appendChild(opt);
|
||||
});
|
||||
renderStore(storeData);
|
||||
}
|
||||
|
||||
function filterStore() {
|
||||
const q = document.getElementById('store-search').value.toLowerCase();
|
||||
const cat = document.getElementById('store-cat').value;
|
||||
const filtered = storeData.filter(a =>
|
||||
(!q || a.name.toLowerCase().includes(q) || a.desc.toLowerCase().includes(q)) &&
|
||||
(!cat || a.cat === cat)
|
||||
);
|
||||
renderStore(filtered);
|
||||
}
|
||||
|
||||
function renderStore(apps) {
|
||||
const grid = document.getElementById('store-grid');
|
||||
grid.innerHTML = apps.map(a => {
|
||||
const ports = Object.entries(a.ports).map(([p, l]) => `${p} (${l})`).join(', ');
|
||||
return `<div class="card" style="padding:12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<div>
|
||||
<strong style="color:#00ff41;font-size:14px">${escHtml(a.name)}</strong>
|
||||
<span style="color:#555;font-size:11px;margin-left:8px">${escHtml(a.cat)}</span>
|
||||
</div>
|
||||
<button class="btn" onclick="installStore('${escHtml(a.name)}')">Install</button>
|
||||
</div>
|
||||
<div style="color:#aaa;font-size:12px;margin:6px 0">${escHtml(a.desc)}</div>
|
||||
<div style="font-size:11px;color:#555">
|
||||
Ports: ${ports}<br>
|
||||
Image: ${escHtml(a.image)}
|
||||
</div>
|
||||
${a.notes ? `<div style="font-size:11px;color:#ffaa00;margin-top:4px">${escHtml(a.notes)}</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (!apps.length) grid.innerHTML = '<div class="info" style="padding:20px">No apps match your search</div>';
|
||||
}
|
||||
|
||||
async function installStore(name) {
|
||||
if (!confirm('Install ' + name + '?\nThis will add it to docker-compose.yml and start it.')) return;
|
||||
const out = document.getElementById('store-output');
|
||||
out.innerHTML = '<span class="info">Installing ' + name + '...</span>';
|
||||
const res = await apiPost('/api/docker/store/install', {name});
|
||||
showResult(res, 'store-output');
|
||||
loadContainers();
|
||||
}
|
||||
|
||||
// ── Git/URL Install ──
|
||||
async function installGit() {
|
||||
const repo = document.getElementById('git-repo').value.trim();
|
||||
const name = document.getElementById('git-name').value.trim();
|
||||
if (!repo) { alert('Enter a git repo URL'); return; }
|
||||
const out = document.getElementById('install-output');
|
||||
out.innerHTML = '<span class="info">Cloning and deploying...</span>';
|
||||
const res = await apiPost('/api/docker/install-git', {repo, name});
|
||||
showResult(res, 'install-output');
|
||||
loadContainers();
|
||||
}
|
||||
|
||||
async function installUrl() {
|
||||
const url = document.getElementById('url-url').value.trim();
|
||||
const name = document.getElementById('url-name').value.trim();
|
||||
if (!url || !name) { alert('Enter URL and app name'); return; }
|
||||
const out = document.getElementById('install-output');
|
||||
out.innerHTML = '<span class="info">Downloading and deploying...</span>';
|
||||
const res = await apiPost('/api/docker/install-url', {url, name});
|
||||
showResult(res, 'install-output');
|
||||
loadContainers();
|
||||
}
|
||||
|
||||
async function installCompose() {
|
||||
const name = document.getElementById('compose-name').value.trim();
|
||||
const compose = document.getElementById('compose-yaml').value;
|
||||
if (!name || !compose) { alert('Enter app name and compose YAML'); return; }
|
||||
const out = document.getElementById('install-output');
|
||||
out.innerHTML = '<span class="info">Deploying compose stack...</span>';
|
||||
const res = await apiPost('/api/docker/install-compose', {name, compose});
|
||||
showResult(res, 'install-output');
|
||||
loadContainers();
|
||||
}
|
||||
|
||||
// Tab key in compose textarea
|
||||
document.getElementById('compose-yaml').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const s = this.selectionStart;
|
||||
this.value = this.value.substring(0, s) + ' ' + this.value.substring(this.selectionEnd);
|
||||
this.selectionStart = this.selectionEnd = s + 2;
|
||||
}
|
||||
});
|
||||
|
||||
loadContainers();
|
||||
</script>
|
||||
{% endblock %}
|
||||
609
setec-web/templates/docs.html
Normal file
609
setec-web/templates/docs.html
Normal file
@@ -0,0 +1,609 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Documentation{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[^] Documentation</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="showDoc('manual')">User Manual</button>
|
||||
<button class="btn" onclick="showDoc('hostlinks')">Host API / SSH Links</button>
|
||||
<button class="btn" onclick="showDoc('troubleshoot')">Troubleshooting Guide</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- USER MANUAL -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="doc-manual" class="card">
|
||||
<div class="card-title">User Manual</div>
|
||||
<div style="font-size:12px;line-height:1.7;color:#ccc">
|
||||
|
||||
<h2>1. Introduction</h2>
|
||||
<p>SETEC LABS Manager is a web-based VPS management panel that connects to your Linux server over SSH.
|
||||
It provides a terminal-style interface for managing security tools, firewalls, DNS, nginx, Docker,
|
||||
email, and more — all from your browser.</p>
|
||||
<p style="color:#888;font-size:11px">Requirements: A Linux VPS (Debian/Ubuntu recommended), SSH key access, Python 3.10+</p>
|
||||
|
||||
<h2>2. Installation</h2>
|
||||
<div style="background:#000;padding:10px;border:1px solid #333;margin:8px 0">
|
||||
<span style="color:#888"># Clone the repository</span><br>
|
||||
<span style="color:#00ff41">git clone https://repo.seteclabs.io/setec/setec-mgr.git</span><br>
|
||||
<span style="color:#00ff41">cd setec-mgr</span><br><br>
|
||||
<span style="color:#888"># Install dependencies</span><br>
|
||||
<span style="color:#00ff41">pip install -r requirements.txt</span><br><br>
|
||||
<span style="color:#888"># Start the manager</span><br>
|
||||
<span style="color:#00ff41">python app.py</span><br><br>
|
||||
<span style="color:#888"># Open in browser</span><br>
|
||||
<span style="color:#00ff41">http://localhost:5000</span>
|
||||
</div>
|
||||
|
||||
<h2>3. Initial Setup (Setup Wizard)</h2>
|
||||
<p>On first launch, click <strong style="color:#00ff41">Setup Wizard</strong> in the sidebar. The wizard walks you through:</p>
|
||||
<ol style="margin-left:15px;line-height:2">
|
||||
<li><strong style="color:#88ff88">Terms of Service</strong> — Read and accept the disclaimer.</li>
|
||||
<li><strong style="color:#88ff88">SSH Keys</strong> — Select existing keys or generate new ones with host-specific guidance.</li>
|
||||
<li><strong style="color:#88ff88">VPS Connection</strong> — Enter your server IP, SSH username, port (2222 recommended), and key path.</li>
|
||||
<li><strong style="color:#88ff88">DNS API</strong> — Select your hosting provider, enter your domain and API key.</li>
|
||||
<li><strong style="color:#88ff88">Paths</strong> — Set web root and Docker Compose file location.</li>
|
||||
<li><strong style="color:#88ff88">Connection Test</strong> — Verify SSH and API connectivity.</li>
|
||||
</ol>
|
||||
<p style="color:#888;font-size:11px">You can re-run the wizard at any time. All settings are also editable from the Settings page.</p>
|
||||
|
||||
<h2>4. Dashboard</h2>
|
||||
<p>The Dashboard shows a real-time overview of your server:</p>
|
||||
<ul style="margin-left:15px;line-height:2">
|
||||
<li><strong>System Info</strong> — Hostname, OS, kernel, uptime</li>
|
||||
<li><strong>Resource Usage</strong> — CPU, RAM, disk, swap</li>
|
||||
<li><strong>Network</strong> — Active connections, listening ports</li>
|
||||
<li><strong>Services</strong> — Status of key services (nginx, sshd, etc.)</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Docker Management</h2>
|
||||
<p>Manage containers, images, volumes, and networks. Start/stop/restart containers, view logs,
|
||||
pull images, and manage your Docker Compose stack.</p>
|
||||
<div style="background:#000;padding:8px;border:1px solid #333;margin:8px 0;font-size:11px;color:#888">
|
||||
Note: SETEC runs services natively with systemd by default. Docker management is provided for
|
||||
users who have containerized workloads.
|
||||
</div>
|
||||
|
||||
<h2>6. DNS Management</h2>
|
||||
<p>View, add, and delete DNS records through your hosting provider's API. Supports:</p>
|
||||
<ul style="margin-left:15px;line-height:2">
|
||||
<li>A, AAAA, CNAME, MX, TXT, NS records</li>
|
||||
<li>10 hosting providers (see Host API / SSH Links tab)</li>
|
||||
<li>Fallback to <code style="color:#00ff41">dig</code> when API is unavailable</li>
|
||||
</ul>
|
||||
|
||||
<h2>7. Nginx Management</h2>
|
||||
<p>Create and manage nginx virtual hosts, enable/disable sites, view access and error logs,
|
||||
test configuration, and manage SSL certificates with Let's Encrypt (certbot).</p>
|
||||
|
||||
<h2>8. SMTP / Email</h2>
|
||||
<p>Configure and manage mail services. View mail queue, check DKIM/SPF/DMARC records,
|
||||
test email delivery, and manage Postfix configuration.</p>
|
||||
|
||||
<h2>9. Firewall</h2>
|
||||
<p>The Firewall page (separate from Security) provides:</p>
|
||||
<ul style="margin-left:15px;line-height:2">
|
||||
<li><strong>Dashboard</strong> — Firewall activity overview and monitoring</li>
|
||||
<li><strong>UFW</strong> — Simplified firewall rule management</li>
|
||||
<li><strong>iptables</strong> — Advanced packet filtering rules</li>
|
||||
<li><strong>nftables</strong> — Modern netfilter framework management</li>
|
||||
<li><strong>firewalld</strong> — Zone-based firewall management</li>
|
||||
<li><strong>CSF</strong> — ConfigServer Security & Firewall</li>
|
||||
<li><strong>Migration</strong> — Convert between UFW and iptables with one click</li>
|
||||
</ul>
|
||||
|
||||
<h2>10. Fail2Ban</h2>
|
||||
<p>Manage Fail2Ban jails, view banned IPs, check jail status, and configure ban rules
|
||||
to protect against brute-force attacks on SSH, nginx, and other services.</p>
|
||||
|
||||
<h2>11. Security Center</h2>
|
||||
<p>The Security page is your central hub for hardening and monitoring:</p>
|
||||
|
||||
<p style="color:#ffaa00;margin-top:10px">Hardening Tools</p>
|
||||
<ul style="margin-left:15px;line-height:2">
|
||||
<li><strong>SSH Hardening</strong> — Disable root login, enforce key auth, change port</li>
|
||||
<li><strong>Kernel Hardening</strong> — Sysctl tweaks for network and memory protection</li>
|
||||
<li><strong>Auto Updates</strong> — Enable unattended-upgrades for security patches</li>
|
||||
<li><strong>.sec Patch System</strong> — Apply SETEC-curated distro-specific security patches</li>
|
||||
</ul>
|
||||
|
||||
<p style="color:#ffaa00;margin-top:10px">Security Applications (each with full management tab)</p>
|
||||
<table style="margin:8px 0">
|
||||
<tr><th>App</th><th>Purpose</th></tr>
|
||||
<tr><td style="color:#00ff41">ClamAV</td><td>Antivirus scanning, quarantine management, scheduled scans</td></tr>
|
||||
<tr><td style="color:#00ff41">rkhunter</td><td>Rootkit detection, file property checks</td></tr>
|
||||
<tr><td style="color:#00ff41">chkrootkit</td><td>Alternative rootkit scanner with expert mode</td></tr>
|
||||
<tr><td style="color:#00ff41">Lynis</td><td>Security auditing and hardening index scoring</td></tr>
|
||||
<tr><td style="color:#00ff41">OSSEC</td><td>Host-based intrusion detection (HIDS), log monitoring, alerts</td></tr>
|
||||
<tr><td style="color:#00ff41">ModSecurity</td><td>Web application firewall (WAF) for nginx, OWASP CRS rules</td></tr>
|
||||
<tr><td style="color:#00ff41">AIDE</td><td>File integrity monitoring, baseline comparison</td></tr>
|
||||
<tr><td style="color:#00ff41">Cowrie</td><td>SSH/Telnet honeypot for attacker monitoring</td></tr>
|
||||
</table>
|
||||
<p style="color:#888;font-size:11px">Each app tab provides: install/uninstall, status, configuration, scanning/auditing, logs, and scheduled tasks.</p>
|
||||
|
||||
<h2>12. Detect</h2>
|
||||
<p>Server detection and fingerprinting. Identifies installed software, open ports,
|
||||
running services, and potential security issues.</p>
|
||||
|
||||
<h2>13. Configs</h2>
|
||||
<p>View and edit critical configuration files directly: sshd_config, nginx.conf,
|
||||
jail.local, and other system configs with syntax-aware editing.</p>
|
||||
|
||||
<h2>14. Files</h2>
|
||||
<p>Browse the server filesystem, view file contents, upload and download files,
|
||||
manage permissions, and navigate directories.</p>
|
||||
|
||||
<h2>15. Terminal</h2>
|
||||
<p>Direct SSH terminal access from the browser. Execute commands on your server
|
||||
with full output display. Useful for tasks not covered by the GUI.</p>
|
||||
|
||||
<h2>16. Settings</h2>
|
||||
<p>Configure all SETEC Manager settings:</p>
|
||||
<ul style="margin-left:15px;line-height:2">
|
||||
<li><strong>VPS Connection</strong> — Host, user, port, SSH key path</li>
|
||||
<li><strong>Hosting Provider API</strong> — Provider selection, API key, documentation links</li>
|
||||
<li><strong>Domain & Paths</strong> — Domain, web root, compose path</li>
|
||||
</ul>
|
||||
|
||||
<h2>17. Front Page</h2>
|
||||
<p>Manage the public-facing landing page for your domain. Edit content,
|
||||
configure styling, and deploy updates.</p>
|
||||
|
||||
<h2>18. Keyboard Shortcuts & Tips</h2>
|
||||
<ul style="margin-left:15px;line-height:2">
|
||||
<li>All actions use AJAX — the page never fully reloads</li>
|
||||
<li>Output panels are scrollable; long scan outputs won't overflow</li>
|
||||
<li>Red text = error, yellow text = warning, green text = success</li>
|
||||
<li>Every destructive action (uninstall, delete, purge) requires confirmation</li>
|
||||
<li>SSH connection is shared — the manager reuses a single SSH session</li>
|
||||
</ul>
|
||||
|
||||
<h2>19. Getting Help</h2>
|
||||
<ul style="margin-left:15px;line-height:2">
|
||||
<li>Official repo: <a href="https://repo.seteclabs.io" target="_blank">repo.seteclabs.io</a></li>
|
||||
<li>GitHub mirror: <a href="https://github.com/DigiJEth" target="_blank">github.com/DigiJEth</a></li>
|
||||
<li>Submit issues and feature requests at the Gitea repo</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- HOST API / SSH LINKS -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="doc-hostlinks" class="card" style="display:none">
|
||||
<div class="card-title">Host API / SSH Links</div>
|
||||
<div style="font-size:12px;line-height:1.7;color:#ccc">
|
||||
|
||||
<p style="color:#888;margin-bottom:15px">Quick-access links to API key generation and SSH key setup guides for every supported hosting provider.</p>
|
||||
|
||||
<!-- Hostinger -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Hostinger</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Key Gen:</td>
|
||||
<td>hPanel → Profile → API Keys → Create new key with DNS permissions<br>
|
||||
<a href="https://developers.hostinger.com" target="_blank">developers.hostinger.com</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>hPanel → VPS → Settings → SSH Keys → Add SSH Key<br>
|
||||
<a href="https://support.hostinger.com/en/articles/1583522-how-to-generate-ssh-keys" target="_blank">support.hostinger.com/.../how-to-generate-ssh-keys</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Cloudflare</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Token:</td>
|
||||
<td>dash.cloudflare.com → My Profile → API Tokens → Create Token → "Edit zone DNS" template<br>
|
||||
<a href="https://developers.cloudflare.com/api" target="_blank">developers.cloudflare.com/api</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH (Tunnel):</td>
|
||||
<td>Cloudflare is a DNS/CDN provider, not a VPS host. If using Cloudflare Tunnel for SSH:<br>
|
||||
<a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/use-cases/ssh/" target="_blank">developers.cloudflare.com/.../ssh</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>DigitalOcean</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Token:</td>
|
||||
<td>cloud.digitalocean.com → API → Tokens → Generate New Token (read+write)<br>
|
||||
<a href="https://docs.digitalocean.com/reference/api" target="_blank">docs.digitalocean.com/reference/api</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>Settings → Security → SSH Keys → Add SSH Key<br>
|
||||
<a href="https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/" target="_blank">docs.digitalocean.com/.../add-ssh-keys</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Vultr -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Vultr</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Key:</td>
|
||||
<td>my.vultr.com → Account → API → Enable API → copy key (whitelist server IP)<br>
|
||||
<a href="https://www.vultr.com/api" target="_blank">vultr.com/api</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>Account → SSH Keys → Add SSH Key<br>
|
||||
<a href="https://docs.vultr.com/how-do-i-generate-ssh-keys" target="_blank">docs.vultr.com/how-do-i-generate-ssh-keys</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Linode (Akamai)</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Token:</td>
|
||||
<td>cloud.linode.com → My Profile → API Tokens → Create Personal Access Token (Domains read/write)<br>
|
||||
<a href="https://www.linode.com/docs/api" target="_blank">linode.com/docs/api</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>Profile → SSH Keys → Add SSH Key (injected into new Linodes at creation)<br>
|
||||
<a href="https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/" target="_blank">linode.com/docs/.../use-public-key-authentication-with-ssh</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>GoDaddy</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Key:</td>
|
||||
<td>developer.godaddy.com → API Keys → Create New API Key. Format: <span style="color:#00ff41">key:secret</span><br>
|
||||
<a href="https://developer.godaddy.com" target="_blank">developer.godaddy.com</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>GoDaddy is primarily a domain registrar. For VPS/dedicated hosting SSH:<br>
|
||||
<a href="https://www.godaddy.com/help/generate-ssh-keys-40767" target="_blank">godaddy.com/help/generate-ssh-keys-40767</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Namecheap</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Key:</td>
|
||||
<td>Profile → Tools → API Access → Enable API (requires IP whitelist)<br>
|
||||
<a href="https://www.namecheap.com/support/api/intro" target="_blank">namecheap.com/support/api/intro</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>Namecheap is primarily a domain registrar. For hosting products:<br>
|
||||
<a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9356/69/how-to-generate-an-ssh-key/" target="_blank">namecheap.com/.../how-to-generate-an-ssh-key</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Hetzner</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Token:</td>
|
||||
<td>dns.hetzner.com → API Tokens → Create new token<br>
|
||||
<a href="https://dns.hetzner.com/api-docs" target="_blank">dns.hetzner.com/api-docs</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>Cloud Console → Security → SSH Keys → Add SSH Key<br>
|
||||
<a href="https://docs.hetzner.com/cloud/servers/getting-started/connecting-to-the-server/" target="_blank">docs.hetzner.com/.../connecting-to-the-server</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- OVH -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>OVH / OVHcloud</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Key:</td>
|
||||
<td>Requires Application Key, Application Secret, and Consumer Key<br>
|
||||
<a href="https://api.ovh.com/createApp" target="_blank">api.ovh.com/createApp</a><br>
|
||||
<a href="https://api.ovh.com" target="_blank">api.ovh.com (documentation)</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>Control Panel → Public Cloud → SSH Keys → Add key<br>
|
||||
<a href="https://help.ovhcloud.com/csm/en-dedicated-servers-creating-ssh-keys" target="_blank">help.ovhcloud.com/.../creating-ssh-keys</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- AWS Route 53 -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>AWS Route 53</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API Key:</td>
|
||||
<td>IAM Console → Users → Create Access Key (needs AmazonRoute53FullAccess). Format: <span style="color:#00ff41">ACCESS_KEY:SECRET_KEY</span><br>
|
||||
<a href="https://docs.aws.amazon.com/Route53/latest/APIReference" target="_blank">docs.aws.amazon.com/Route53/latest/APIReference</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>EC2 Console → Key Pairs → Create/Import key pair (downloads .pem file)<br>
|
||||
<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html" target="_blank">docs.aws.amazon.com/.../ec2-key-pairs.html</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Contabo -->
|
||||
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
|
||||
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Contabo</strong></p>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="color:#888;width:120px">API:</td>
|
||||
<td>Contabo does not provide a DNS API. Use a third-party DNS provider (Cloudflare, etc.) or manage records manually.<br>
|
||||
<a href="https://api.contabo.com" target="_blank">api.contabo.com (server management API only)</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#888">SSH Key Guide:</td>
|
||||
<td>No panel-based SSH key manager. Generate keys locally and copy with <code style="color:#00ff41">ssh-copy-id</code><br>
|
||||
<a href="https://contabo.com/blog/establishing-connection-server-ssh/" target="_blank">contabo.com/blog/establishing-connection-server-ssh</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Generic SSH keygen -->
|
||||
<div style="border:1px solid #00ff41;padding:12px;margin-top:15px;background:#0a0a0a">
|
||||
<p style="color:#88ff88;font-size:13px;margin-bottom:8px"><strong>Universal: Generate SSH Keys (any provider)</strong></p>
|
||||
<div style="background:#000;padding:10px;border:1px solid #333;margin:8px 0;font-size:11px">
|
||||
<span style="color:#888"># Generate ed25519 key pair (recommended)</span><br>
|
||||
<span style="color:#00ff41">ssh-keygen -t ed25519 -f C:/keys/setec -N ""</span><br><br>
|
||||
<span style="color:#888"># Copy public key to server</span><br>
|
||||
<span style="color:#00ff41">ssh-copy-id -i C:/keys/setec.pub -p 2222 root@YOUR_SERVER_IP</span><br><br>
|
||||
<span style="color:#888"># Test connection</span><br>
|
||||
<span style="color:#00ff41">ssh -i C:/keys/setec -p 2222 root@YOUR_SERVER_IP</span><br><br>
|
||||
<span style="color:#888"># (Alternative) RSA 4096-bit key</span><br>
|
||||
<span style="color:#00ff41">ssh-keygen -t rsa -b 4096 -f C:/keys/setec -N ""</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- TROUBLESHOOTING GUIDE -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="doc-troubleshoot" class="card" style="display:none">
|
||||
<div class="card-title">Troubleshooting Guide</div>
|
||||
<div style="font-size:12px;line-height:1.7;color:#ccc">
|
||||
|
||||
<!-- SSH Connection -->
|
||||
<h2>SSH Connection Issues</h2>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Connection refused" or "Connection timed out"</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Verify the server IP is correct and the VPS is powered on</li>
|
||||
<li>Check the SSH port matches what's configured: <code style="color:#00ff41">nc -zv YOUR_IP YOUR_PORT</code></li>
|
||||
<li>Confirm the port is open on the server firewall:
|
||||
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
|
||||
<span style="color:#00ff41">ufw status</span> or <span style="color:#00ff41">iptables -L -n | grep YOUR_PORT</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>Check if your home IP is blocked (fail2ban, CSF, or hosting provider firewall)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Permission denied (publickey)"</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Verify the key path in Settings points to your <strong>private</strong> key (not .pub)</li>
|
||||
<li>Check the public key is in the server's authorized_keys:
|
||||
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
|
||||
<span style="color:#00ff41">cat ~/.ssh/authorized_keys</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>Fix permissions on the server:
|
||||
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
|
||||
<span style="color:#00ff41">chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>Test manually with verbose output:
|
||||
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
|
||||
<span style="color:#00ff41">ssh -i /path/to/key -p PORT user@IP -vvv</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Host key verification failed"</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>The server's fingerprint changed (reinstall, IP reassignment, or MITM)</li>
|
||||
<li>Remove the old key: <code style="color:#00ff41">ssh-keygen -R YOUR_IP</code></li>
|
||||
<li>Reconnect and verify the new fingerprint with your hosting provider</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
|
||||
<p style="color:#ffaa00;margin-bottom:5px"><strong>SSH connects but commands time out</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Server may be under heavy load — check CPU/RAM in your hosting panel</li>
|
||||
<li>Long-running commands (full scans, package installs) have extended timeouts but may still exceed them</li>
|
||||
<li>Try running the command directly from Terminal page for real-time output</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- DNS API -->
|
||||
<h2>DNS API Issues</h2>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "HTTP 401" or "HTTP 403"</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>API key is invalid, expired, or has insufficient permissions</li>
|
||||
<li>Regenerate a new API key from your provider's dashboard (see Host API Links)</li>
|
||||
<li>Ensure the key has DNS read/write permissions</li>
|
||||
<li>Some providers (Vultr, Namecheap) require IP whitelisting</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "HTTP 530" or Cloudflare blocking API calls</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>If your domain uses Cloudflare as a proxy, API calls from your VPS may be intercepted</li>
|
||||
<li>SETEC Manager routes API calls through your VPS via SSH to bypass this</li>
|
||||
<li>If still failing, try making the API call directly from your local machine</li>
|
||||
<li>Check if the provider's API endpoint is behind Cloudflare</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
|
||||
<p style="color:#ffaa00;margin-bottom:5px"><strong>DNS changes not propagating</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>DNS propagation takes time (minutes to 48 hours depending on TTL)</li>
|
||||
<li>Check current state: <code style="color:#00ff41">dig +short A yourdomain.com</code></li>
|
||||
<li>Check with different resolvers: <code style="color:#00ff41">dig @8.8.8.8 +short A yourdomain.com</code></li>
|
||||
<li>Lower your TTL to 300 before making changes for faster propagation</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Security Tools -->
|
||||
<h2>Security Tool Issues</h2>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Tool install fails with "dpkg lock" error</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Another package operation is in progress (apt update, unattended-upgrades)</li>
|
||||
<li>Wait a few minutes and retry, or check:
|
||||
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
|
||||
<span style="color:#00ff41">lsof /var/lib/dpkg/lock-frontend</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>If the process is stuck, kill it (last resort):
|
||||
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
|
||||
<span style="color:#00ff41">kill -9 PID && dpkg --configure -a</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>ClamAV: "freshclam" or virus DB errors</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Stop freshclam service before manual update: <code style="color:#00ff41">systemctl stop clamav-freshclam</code></li>
|
||||
<li>Run manual update: <code style="color:#00ff41">freshclam</code></li>
|
||||
<li>ClamAV CDN may rate-limit — wait and retry in 30 minutes</li>
|
||||
<li>Check if DNS resolves: <code style="color:#00ff41">dig database.clamav.net</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
|
||||
<p style="color:#ffaa00;margin-bottom:5px"><strong>Scans/audits appear to hang or run forever</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Full system scans (ClamAV, Lynis, AIDE) can take 10-60+ minutes</li>
|
||||
<li>Use "Quick Scan" options when available for faster results</li>
|
||||
<li>Check server load — scans are CPU-intensive</li>
|
||||
<li>For long scans, use the Terminal page for real-time output</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Firewall -->
|
||||
<h2>Firewall Issues</h2>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Locked out of server after firewall change</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li style="color:#ff4444"><strong>Prevention:</strong> ALWAYS allow your SSH port before enabling the firewall</li>
|
||||
<li>Use your hosting provider's console/VNC access to regain control</li>
|
||||
<li>From console: <code style="color:#00ff41">ufw allow 2222/tcp && ufw reload</code></li>
|
||||
<li>Or disable the firewall entirely: <code style="color:#00ff41">ufw disable</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
|
||||
<p style="color:#ffaa00;margin-bottom:5px"><strong>Multiple firewalls conflicting</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Only run ONE firewall at a time (UFW, iptables raw, nftables, firewalld, or CSF)</li>
|
||||
<li>UFW is a frontend for iptables — they share the same backend</li>
|
||||
<li>Use the Migration tabs (UFW↔iptables) to safely switch</li>
|
||||
<li>Check what's active: <code style="color:#00ff41">ufw status</code>, <code style="color:#00ff41">iptables -L -n</code>, <code style="color:#00ff41">nft list ruleset</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Nginx / SSL -->
|
||||
<h2>Nginx / SSL Issues</h2>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Certbot SSL fails: "DNS problem: NXDOMAIN"</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>The domain/subdomain doesn't have a DNS A record pointing to your server</li>
|
||||
<li>Add the A record first, wait for propagation, then retry certbot</li>
|
||||
<li>Verify: <code style="color:#00ff41">dig +short A subdomain.yourdomain.com</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
|
||||
<p style="color:#ff4444;margin-bottom:5px"><strong>Nginx won't start: "address already in use"</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Another process is using port 80/443: <code style="color:#00ff41">ss -tlnp | grep ':80\|:443'</code></li>
|
||||
<li>Common culprit: Apache. Stop it: <code style="color:#00ff41">systemctl stop apache2 && systemctl disable apache2</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- General -->
|
||||
<h2>General Issues</h2>
|
||||
|
||||
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
|
||||
<p style="color:#ffaa00;margin-bottom:5px"><strong>Manager shows "Loading..." forever</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>SSH connection dropped — refresh the page to reconnect</li>
|
||||
<li>Check that <code style="color:#00ff41">python app.py</code> is still running in your terminal</li>
|
||||
<li>Check browser console (F12) for JavaScript errors</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
|
||||
<p style="color:#ffaa00;margin-bottom:5px"><strong>Settings not saving / resetting on restart</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Config is stored at <code style="color:#00ff41">~/.setec-mgr/config.json</code></li>
|
||||
<li>Check file permissions: <code style="color:#00ff41">ls -la ~/.setec-mgr/</code></li>
|
||||
<li>View current config: <code style="color:#00ff41">cat ~/.setec-mgr/config.json</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid #00ff41;padding:12px;margin-top:20px;background:#0a0a0a">
|
||||
<p style="color:#88ff88;margin-bottom:5px"><strong>Still need help?</strong></p>
|
||||
<ul style="margin-left:15px;color:#888;line-height:1.8">
|
||||
<li>Submit a ticket: <a href="https://repo.seteclabs.io" target="_blank">repo.seteclabs.io</a></li>
|
||||
<li>GitHub mirror: <a href="https://github.com/DigiJEth" target="_blank">github.com/DigiJEth</a></li>
|
||||
<li>Include: error message, server OS, SETEC Manager version, and steps to reproduce</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function showDoc(id) {
|
||||
document.getElementById('doc-manual').style.display = (id === 'manual') ? 'block' : 'none';
|
||||
document.getElementById('doc-hostlinks').style.display = (id === 'hostlinks') ? 'block' : 'none';
|
||||
document.getElementById('doc-troubleshoot').style.display = (id === 'troubleshoot') ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
168
setec-web/templates/fail2ban.html
Normal file
168
setec-web/templates/fail2ban.html
Normal file
@@ -0,0 +1,168 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Fail2Ban{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[!] Fail2Ban</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadStatus()">Refresh</button>
|
||||
<button class="btn" onclick="loadAllJails()">All Jails Detail</button>
|
||||
<button class="btn" onclick="loadLog()">View Log</button>
|
||||
<button class="btn" onclick="loadConfig()">Edit Config</button>
|
||||
<button class="btn btn-warn" onclick="reloadF2B()">Reload</button>
|
||||
<button class="btn btn-danger" onclick="unbanAll()">Unban All</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Status</div>
|
||||
<div class="output" id="status-output"><span class="info">Loading...</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Jail Details</div>
|
||||
<div class="toolbar">
|
||||
<select id="jail-select" onchange="loadJail()">
|
||||
<option value="">-- select jail --</option>
|
||||
<option value="sshd">sshd</option>
|
||||
<option value="nginx-http-auth">nginx-http-auth</option>
|
||||
<option value="nginx-botsearch">nginx-botsearch</option>
|
||||
<option value="nginx-badbots">nginx-badbots</option>
|
||||
<option value="postfix">postfix</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="output" id="jail-output"><span class="info">Select a jail</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Ban IP</div>
|
||||
<label>Jail</label>
|
||||
<select id="ban-jail">
|
||||
<option value="sshd">sshd</option>
|
||||
<option value="nginx-http-auth">nginx-http-auth</option>
|
||||
<option value="nginx-botsearch">nginx-botsearch</option>
|
||||
<option value="nginx-badbots">nginx-badbots</option>
|
||||
<option value="postfix">postfix</option>
|
||||
</select>
|
||||
<label>IP Address</label>
|
||||
<input type="text" id="ban-ip" placeholder="1.2.3.4" style="width:200px">
|
||||
<br><br>
|
||||
<button class="btn btn-danger" onclick="banIP()">Ban</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Unban IP</div>
|
||||
<label>Jail</label>
|
||||
<select id="unban-jail">
|
||||
<option value="sshd">sshd</option>
|
||||
<option value="nginx-http-auth">nginx-http-auth</option>
|
||||
<option value="nginx-botsearch">nginx-botsearch</option>
|
||||
<option value="nginx-badbots">nginx-badbots</option>
|
||||
<option value="postfix">postfix</option>
|
||||
</select>
|
||||
<label>IP Address</label>
|
||||
<input type="text" id="unban-ip" placeholder="1.2.3.4" style="width:200px">
|
||||
<br><br>
|
||||
<button class="btn" onclick="unbanIP()">Unban</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="config-card" style="display:none">
|
||||
<div class="card-title">jail.local</div>
|
||||
<textarea id="config-editor" rows="20" style="width:100%;tab-size:4" spellcheck="false"></textarea>
|
||||
<br>
|
||||
<button class="btn" onclick="saveConfig()">Save & Reload</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output / Log</div>
|
||||
<div class="output" id="output" style="max-height:500px"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function loadStatus() {
|
||||
const res = await apiGet('/api/fail2ban/status');
|
||||
showResult(res, 'status-output');
|
||||
}
|
||||
|
||||
async function loadJail() {
|
||||
const name = document.getElementById('jail-select').value;
|
||||
if (!name) return;
|
||||
const res = await apiGet('/api/fail2ban/jail/' + name);
|
||||
showResult(res, 'jail-output');
|
||||
}
|
||||
|
||||
async function loadAllJails() {
|
||||
const res = await apiGet('/api/fail2ban/jails');
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function banIP() {
|
||||
const jail = document.getElementById('ban-jail').value;
|
||||
const ip = document.getElementById('ban-ip').value.trim();
|
||||
if (!ip) { alert('Enter an IP'); return; }
|
||||
const res = await apiPost('/api/fail2ban/ban', {jail, ip});
|
||||
showResult(res);
|
||||
loadJail();
|
||||
}
|
||||
|
||||
async function unbanIP() {
|
||||
const jail = document.getElementById('unban-jail').value;
|
||||
const ip = document.getElementById('unban-ip').value.trim();
|
||||
if (!ip) { alert('Enter an IP'); return; }
|
||||
const res = await apiPost('/api/fail2ban/unban', {jail, ip});
|
||||
showResult(res);
|
||||
loadJail();
|
||||
}
|
||||
|
||||
async function unbanAll() {
|
||||
if (!confirm('Unban ALL IPs from ALL jails?')) return;
|
||||
const res = await apiPost('/api/fail2ban/unban-all');
|
||||
showResult(res);
|
||||
loadStatus();
|
||||
}
|
||||
|
||||
async function reloadF2B() {
|
||||
const res = await apiPost('/api/fail2ban/reload');
|
||||
showResult(res);
|
||||
loadStatus();
|
||||
}
|
||||
|
||||
async function loadLog() {
|
||||
const res = await apiGet('/api/fail2ban/log?lines=50');
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
document.getElementById('config-card').style.display = '';
|
||||
const res = await apiGet('/api/fail2ban/config');
|
||||
if (res.ok) {
|
||||
document.getElementById('config-editor').value = res.data.stdout || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const content = document.getElementById('config-editor').value;
|
||||
if (!confirm('Save config and reload fail2ban?')) return;
|
||||
const res = await apiPost('/api/fail2ban/config/save', {content});
|
||||
showResult(res);
|
||||
loadStatus();
|
||||
}
|
||||
|
||||
document.getElementById('config-editor').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const s = this.selectionStart;
|
||||
this.value = this.value.substring(0, s) + ' ' + this.value.substring(this.selectionEnd);
|
||||
this.selectionStart = this.selectionEnd = s + 4;
|
||||
}
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
loadStatus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
93
setec-web/templates/files.html
Normal file
93
setec-web/templates/files.html
Normal file
@@ -0,0 +1,93 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Files{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[/] File Manager</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<input type="text" id="file-path" value="/var/www" style="width:400px">
|
||||
<button class="btn" onclick="listFiles()">List</button>
|
||||
<button class="btn" onclick="readFile()">Read File</button>
|
||||
<button class="btn" onclick="mkdirRemote()">mkdir</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Directory Listing</div>
|
||||
<div class="output" id="dir-output" style="max-height:600px;cursor:pointer" onclick="handleDirClick(event)">
|
||||
<span class="info">Enter a path and click List</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">File Editor</div>
|
||||
<label>Path</label>
|
||||
<input type="text" id="edit-path" style="width:100%">
|
||||
<textarea id="edit-content" rows="20" style="width:100%;resize:vertical"></textarea>
|
||||
<br>
|
||||
<button class="btn" onclick="saveFile()">Save File</button>
|
||||
<button class="btn btn-danger" onclick="deleteFile()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output</div>
|
||||
<div class="output" id="output"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function listFiles() {
|
||||
const path = document.getElementById('file-path').value;
|
||||
const res = await apiGet('/api/files/list?path='+encodeURIComponent(path));
|
||||
showResult(res, 'dir-output');
|
||||
}
|
||||
|
||||
async function readFile() {
|
||||
const path = document.getElementById('file-path').value;
|
||||
const res = await apiGet('/api/files/read?path='+encodeURIComponent(path));
|
||||
if (res.ok) {
|
||||
document.getElementById('edit-path').value = path;
|
||||
document.getElementById('edit-content').value = res.data.stdout || '';
|
||||
}
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
const path = document.getElementById('edit-path').value;
|
||||
const content = document.getElementById('edit-content').value;
|
||||
if (!path) { alert('Enter file path'); return; }
|
||||
if (!confirm('Save to '+path+'?')) return;
|
||||
const res = await apiPost('/api/files/write', {path, content});
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
const path = document.getElementById('edit-path').value || document.getElementById('file-path').value;
|
||||
if (!path) return;
|
||||
if (!confirm('DELETE '+path+'? This cannot be undone!')) return;
|
||||
const res = await apiDelete('/api/files/delete', {path});
|
||||
showResult(res);
|
||||
listFiles();
|
||||
}
|
||||
|
||||
async function mkdirRemote() {
|
||||
const path = document.getElementById('file-path').value;
|
||||
if (!path) return;
|
||||
const res = await apiPost('/api/files/mkdir', {path});
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
function handleDirClick(event) {
|
||||
// Allow clicking on filenames in the listing
|
||||
const text = window.getSelection().toString().trim();
|
||||
if (text && !text.includes('\n')) {
|
||||
const base = document.getElementById('file-path').value.replace(/\/$/,'');
|
||||
document.getElementById('file-path').value = base + '/' + text;
|
||||
}
|
||||
}
|
||||
|
||||
listFiles();
|
||||
</script>
|
||||
{% endblock %}
|
||||
990
setec-web/templates/firewall.html
Normal file
990
setec-web/templates/firewall.html
Normal file
@@ -0,0 +1,990 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Firewall{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[|] Firewall Manager</h1>
|
||||
|
||||
<!-- Tab navigation -->
|
||||
<div class="toolbar" id="tabs">
|
||||
<button class="btn" onclick="showTab('dashboard')" id="tab-dashboard">Dashboard</button>
|
||||
<button class="btn" onclick="showTab('ufw')" id="tab-ufw">UFW</button>
|
||||
<button class="btn" onclick="showTab('iptables')" id="tab-iptables">iptables</button>
|
||||
<button class="btn" onclick="showTab('nftables')" id="tab-nftables">nftables</button>
|
||||
<button class="btn" onclick="showTab('firewalld')" id="tab-firewalld">firewalld</button>
|
||||
<button class="btn" onclick="showTab('csf')" id="tab-csf">CSF</button>
|
||||
<button class="btn" onclick="showTab('ufw2ip')" id="tab-ufw2ip">UFW → IP</button>
|
||||
<button class="btn" onclick="showTab('ip2ufw')" id="tab-ip2ufw">IP → UFW</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ DASHBOARD TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-dashboard">
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Firewall Detection</div>
|
||||
<div id="fw-detect" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwDetect()">Detect Firewalls</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Open Ports</div>
|
||||
<div id="fw-ports" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwPorts()">Scan Ports</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Active Connections</div>
|
||||
<div id="fw-conns" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwConns()">Refresh</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Connection Stats by State</div>
|
||||
<div id="fw-connstats" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwConnStats()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Top Connections by IP</div>
|
||||
<div id="fw-topip" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwTopIPs()">Refresh</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Recent Blocked / Dropped</div>
|
||||
<div id="fw-blocked" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwBlocked()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Firewall Log</div>
|
||||
<div id="fw-log" class="output" style="max-height:400px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwLog()">Load Log</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ UFW TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-ufw" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-title">UFW Status</div>
|
||||
<div id="ufw-status" class="output" style="max-height:400px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="ufwStatus()">Refresh</button>
|
||||
<button class="btn btn-warn" onclick="ufwEnable()">Enable UFW</button>
|
||||
<button class="btn btn-danger" onclick="ufwDisable()">Disable UFW</button>
|
||||
<button class="btn" onclick="ufwNumbered()">Numbered Rules</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Add Rule</div>
|
||||
<label>Rule (e.g. "allow 8080/tcp", "deny from 1.2.3.4")</label>
|
||||
<input type="text" id="ufw-rule" placeholder="allow 443/tcp" style="width:100%;">
|
||||
<button class="btn" onclick="ufwAdd()" style="margin-top:8px;">Add Rule</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Delete Rule</div>
|
||||
<label>Rule to delete (e.g. "allow 8080/tcp")</label>
|
||||
<input type="text" id="ufw-del-rule" placeholder="allow 8080/tcp" style="width:100%;">
|
||||
<button class="btn btn-danger" onclick="ufwDel()" style="margin-top:8px;">Delete Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Presets</div>
|
||||
<button class="btn" onclick="ufwPreset('webserver')">Web Server (80, 443)</button>
|
||||
<button class="btn" onclick="ufwPreset('mailserver')">Mail Server (25, 465, 587, 993)</button>
|
||||
<button class="btn btn-danger" onclick="ufwPreset('lockdown')">Lockdown (SSH only)</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Defaults</div>
|
||||
<div id="ufw-defaults" class="output" style="max-height:150px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="ufwDefault('deny','incoming')">Default Deny In</button>
|
||||
<button class="btn" onclick="ufwDefault('allow','outgoing')">Default Allow Out</button>
|
||||
<button class="btn btn-danger" onclick="ufwDefault('deny','outgoing')">Default Deny Out</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">UFW Application Profiles</div>
|
||||
<div id="ufw-apps" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="ufwAppList()">List App Profiles</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">UFW Log</div>
|
||||
<div id="ufw-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="ufwLog()">View Log</button>
|
||||
<button class="btn" onclick="ufwLogLevel('on')">Logging On</button>
|
||||
<button class="btn" onclick="ufwLogLevel('high')">Logging High</button>
|
||||
<button class="btn btn-danger" onclick="ufwLogLevel('off')">Logging Off</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ IPTABLES TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-iptables" style="display:none;">
|
||||
<div id="ipt-not-installed" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-title">iptables</div>
|
||||
<p style="color:#888;margin-bottom:10px;">iptables is the traditional Linux packet filter. It is usually pre-installed.</p>
|
||||
<div id="ipt-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<button class="btn btn-warn" onclick="iptInstall()">Install iptables</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ipt-main">
|
||||
<div class="card">
|
||||
<div class="card-title">iptables Rules (filter)</div>
|
||||
<div id="ipt-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="iptList()">Filter Table</button>
|
||||
<button class="btn" onclick="iptListNat()">NAT Table</button>
|
||||
<button class="btn" onclick="iptListMangle()">Mangle Table</button>
|
||||
<button class="btn" onclick="iptCounters()">Counters</button>
|
||||
<button class="btn" onclick="iptIp6()">IPv6 Rules</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Add Rule</div>
|
||||
<label>Chain</label>
|
||||
<select id="ipt-chain" style="width:120px;">
|
||||
<option>INPUT</option>
|
||||
<option>OUTPUT</option>
|
||||
<option>FORWARD</option>
|
||||
</select>
|
||||
<label>Rule (e.g. "-p tcp --dport 80 -j ACCEPT")</label>
|
||||
<input type="text" id="ipt-rule" placeholder="-p tcp --dport 80 -j ACCEPT" style="width:100%;">
|
||||
<div style="margin-top:8px;">
|
||||
<button class="btn" onclick="iptAdd()">Append Rule</button>
|
||||
<label style="display:inline;">Position</label>
|
||||
<input type="number" id="ipt-pos" value="1" style="width:50px;">
|
||||
<button class="btn" onclick="iptInsert()">Insert at Position</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Delete Rule</div>
|
||||
<label>Chain</label>
|
||||
<select id="ipt-del-chain" style="width:120px;">
|
||||
<option>INPUT</option>
|
||||
<option>OUTPUT</option>
|
||||
<option>FORWARD</option>
|
||||
</select>
|
||||
<label>Rule Number</label>
|
||||
<input type="number" id="ipt-del-num" value="1" style="width:80px;">
|
||||
<button class="btn btn-danger" onclick="iptDelete()" style="margin-top:8px;">Delete Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Chain Policy</div>
|
||||
<label>Chain</label>
|
||||
<select id="ipt-pol-chain" style="width:120px;">
|
||||
<option>INPUT</option>
|
||||
<option>OUTPUT</option>
|
||||
<option>FORWARD</option>
|
||||
</select>
|
||||
<label>Policy</label>
|
||||
<select id="ipt-pol-target" style="width:120px;">
|
||||
<option>ACCEPT</option>
|
||||
<option>DROP</option>
|
||||
</select>
|
||||
<button class="btn btn-warn" onclick="iptPolicy()" style="margin-top:8px;">Set Policy</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Quick Actions</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<label>IP Address</label>
|
||||
<input type="text" id="ipt-ip" placeholder="1.2.3.4" style="width:160px;">
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-danger" onclick="iptBlockIP()">Block IP</button>
|
||||
<button class="btn" onclick="iptUnblockIP()">Unblock IP</button>
|
||||
<button class="btn" onclick="iptListBlocked()">Show Blocked</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Persistence</div>
|
||||
<div id="ipt-persist" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-warn" onclick="iptSave()">Save Rules</button>
|
||||
<button class="btn" onclick="iptRestore()">Restore Saved</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Flush</div>
|
||||
<div id="ipt-flush-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-danger" onclick="iptFlush()">Flush All Rules</button>
|
||||
<button class="btn" onclick="iptZero()">Zero Counters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">iptables Log</div>
|
||||
<div id="ipt-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="iptLog()">View Log</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ NFTABLES TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-nftables" style="display:none;">
|
||||
<div id="nft-not-installed" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-title">nftables</div>
|
||||
<p style="color:#888;margin-bottom:10px;">nftables is the modern replacement for iptables. Provides better performance and a cleaner syntax.</p>
|
||||
<div id="nft-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<button class="btn btn-warn" onclick="nftInstall()">Install nftables</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="nft-main">
|
||||
<div class="card">
|
||||
<div class="card-title">nftables Ruleset</div>
|
||||
<div id="nft-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="nftList()">Full Ruleset</button>
|
||||
<button class="btn" onclick="nftTables()">Tables</button>
|
||||
<button class="btn" onclick="nftCounters()">Counters</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">List Chains</div>
|
||||
<div id="nft-chains" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<label>Table (e.g. "inet filter")</label>
|
||||
<input type="text" id="nft-table" value="inet filter" style="width:200px;">
|
||||
<button class="btn" onclick="nftChains()" style="margin-top:8px;">List Chains</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Add Rule</div>
|
||||
<label>Table</label>
|
||||
<input type="text" id="nft-add-table" value="inet filter" style="width:180px;">
|
||||
<label>Chain</label>
|
||||
<input type="text" id="nft-add-chain" value="input" style="width:180px;">
|
||||
<label>Rule (e.g. "tcp dport 80 accept")</label>
|
||||
<input type="text" id="nft-add-rule" placeholder="tcp dport 80 accept" style="width:100%;">
|
||||
<button class="btn" onclick="nftAddRule()" style="margin-top:8px;">Add Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Delete Rule</div>
|
||||
<label>Table</label>
|
||||
<input type="text" id="nft-del-table" value="inet filter" style="width:180px;">
|
||||
<label>Chain</label>
|
||||
<input type="text" id="nft-del-chain" value="input" style="width:180px;">
|
||||
<label>Handle Number</label>
|
||||
<input type="number" id="nft-del-handle" style="width:80px;">
|
||||
<button class="btn btn-danger" onclick="nftDelRule()" style="margin-top:8px;">Delete Rule</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Create Table / Chain</div>
|
||||
<label>Family</label>
|
||||
<select id="nft-family" style="width:100px;">
|
||||
<option>inet</option>
|
||||
<option>ip</option>
|
||||
<option>ip6</option>
|
||||
<option>bridge</option>
|
||||
</select>
|
||||
<label>Table Name</label>
|
||||
<input type="text" id="nft-new-table" placeholder="mytable" style="width:150px;">
|
||||
<button class="btn" onclick="nftCreateTable()" style="margin-top:8px;">Create Table</button>
|
||||
<div style="margin-top:10px;border-top:1px solid #333;padding-top:8px;">
|
||||
<label>Chain Name</label>
|
||||
<input type="text" id="nft-new-chain" placeholder="mychain" style="width:150px;">
|
||||
<label>Hook</label>
|
||||
<select id="nft-hook" style="width:100px;">
|
||||
<option>input</option>
|
||||
<option>output</option>
|
||||
<option>forward</option>
|
||||
<option>prerouting</option>
|
||||
<option>postrouting</option>
|
||||
</select>
|
||||
<button class="btn" onclick="nftCreateChain()" style="margin-top:8px;">Create Chain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Persistence</div>
|
||||
<div id="nft-persist" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-warn" onclick="nftSave()">Save Ruleset</button>
|
||||
<button class="btn" onclick="nftRestore()">Restore Saved</button>
|
||||
<button class="btn" onclick="nftConfig()">View Config File</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Flush</div>
|
||||
<div id="nft-flush-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<button class="btn btn-danger" onclick="nftFlush()">Flush All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ FIREWALLD TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-firewalld" style="display:none;">
|
||||
<div id="fwd-not-installed" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-title">firewalld</div>
|
||||
<p style="color:#888;margin-bottom:10px;">firewalld is a zone-based firewall manager. Common on RHEL/CentOS but also available on Debian/Ubuntu.</p>
|
||||
<div id="fwd-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<button class="btn btn-warn" onclick="fwdInstall()">Install firewalld</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fwd-main">
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">firewalld Status</div>
|
||||
<div id="fwd-status" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="fwdStatus()">Refresh</button>
|
||||
<button class="btn btn-warn" onclick="fwdReload()">Reload</button>
|
||||
<button class="btn btn-danger" onclick="fwdPanicOn()">Panic On</button>
|
||||
<button class="btn" onclick="fwdPanicOff()">Panic Off</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Zones</div>
|
||||
<div id="fwd-zones" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwdZones()">List Zones</button>
|
||||
<div style="margin-top:8px;">
|
||||
<label>Zone</label>
|
||||
<input type="text" id="fwd-zone" value="public" style="width:120px;">
|
||||
<button class="btn" onclick="fwdZoneInfo()">Zone Info</button>
|
||||
<button class="btn btn-warn" onclick="fwdSetDefault()">Set as Default</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Services</div>
|
||||
<div id="fwd-services" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<label>Service (e.g. http, https, ssh, smtp)</label>
|
||||
<input type="text" id="fwd-service" placeholder="http" style="width:150px;">
|
||||
<div class="toolbar" style="margin-top:8px;">
|
||||
<button class="btn" onclick="fwdServicesList()">List Available</button>
|
||||
<button class="btn btn-warn" onclick="fwdAddService()">Add Service</button>
|
||||
<button class="btn btn-danger" onclick="fwdRemoveService()">Remove Service</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Ports</div>
|
||||
<div id="fwd-ports" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<label>Port (e.g. 8080/tcp)</label>
|
||||
<input type="text" id="fwd-port" placeholder="8080/tcp" style="width:150px;">
|
||||
<div class="toolbar" style="margin-top:8px;">
|
||||
<button class="btn btn-warn" onclick="fwdAddPort()">Add Port</button>
|
||||
<button class="btn btn-danger" onclick="fwdRemovePort()">Remove Port</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Rich Rules</div>
|
||||
<div id="fwd-rich" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<label>Rich Rule</label>
|
||||
<input type="text" id="fwd-rich-rule" placeholder='rule family="ipv4" source address="1.2.3.4" drop' style="width:100%;">
|
||||
<div class="toolbar" style="margin-top:8px;">
|
||||
<button class="btn btn-warn" onclick="fwdAddRich()">Add Rich Rule</button>
|
||||
<button class="btn btn-danger" onclick="fwdRemoveRich()">Remove Rich Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Block / Unblock IP</div>
|
||||
<div id="fwd-block" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<label>IP Address</label>
|
||||
<input type="text" id="fwd-ip" placeholder="1.2.3.4" style="width:160px;">
|
||||
<div class="toolbar" style="margin-top:8px;">
|
||||
<button class="btn btn-danger" onclick="fwdBlockIP()">Block IP</button>
|
||||
<button class="btn" onclick="fwdUnblockIP()">Unblock IP</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">firewalld Log</div>
|
||||
<div id="fwd-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="fwdLog()">View Log</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ CSF TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-csf" style="display:none;">
|
||||
<div id="csf-not-installed" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-title">CSF (ConfigServer Security & Firewall)</div>
|
||||
<p style="color:#888;margin-bottom:10px;">CSF is a stateful packet inspection firewall with login/intrusion detection (LFD). Popular on cPanel servers but works standalone.</p>
|
||||
<div id="csf-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<button class="btn btn-warn" onclick="csfInstall()">Install CSF</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="csf-main">
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">CSF Status</div>
|
||||
<div id="csf-status" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="csfStatus()">Refresh</button>
|
||||
<button class="btn btn-warn" onclick="csfStart()">Start</button>
|
||||
<button class="btn btn-danger" onclick="csfStop()">Stop (Flush)</button>
|
||||
<button class="btn" onclick="csfRestart()">Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Test iptables Modules</div>
|
||||
<div id="csf-test" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="csfTest()">Run Test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Rules</div>
|
||||
<div id="csf-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="csfList()">List All Rules</button>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Allow / Deny IP</div>
|
||||
<div id="csf-ip-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<label>IP Address</label>
|
||||
<input type="text" id="csf-ip" placeholder="1.2.3.4" style="width:160px;">
|
||||
<label>Comment</label>
|
||||
<input type="text" id="csf-comment" placeholder="reason" style="width:200px;">
|
||||
<div class="toolbar" style="margin-top:8px;">
|
||||
<button class="btn" onclick="csfAllow()">Allow</button>
|
||||
<button class="btn btn-danger" onclick="csfDeny()">Deny</button>
|
||||
<button class="btn" onclick="csfRemove()">Remove</button>
|
||||
<button class="btn" onclick="csfGrep()">Search IP</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Temporary Rules</div>
|
||||
<div id="csf-temp" class="output" style="max-height:200px;margin-bottom:10px;"></div>
|
||||
<label>TTL (seconds)</label>
|
||||
<input type="number" id="csf-ttl" value="3600" style="width:100px;">
|
||||
<div class="toolbar" style="margin-top:8px;">
|
||||
<button class="btn" onclick="csfTempAllow()">Temp Allow</button>
|
||||
<button class="btn btn-danger" onclick="csfTempDeny()">Temp Deny</button>
|
||||
<button class="btn" onclick="csfTempList()">List Temp Rules</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Configuration</div>
|
||||
<div id="csf-config" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="csfConfig()">View Key Settings</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">LFD Log</div>
|
||||
<div id="csf-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
|
||||
<button class="btn" onclick="csfLog()">View Log</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ UFW → IPTABLES TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-ufw2ip" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-title">Migrate: UFW → iptables</div>
|
||||
<p style="color:#888;margin-bottom:15px;">
|
||||
This will disable UFW and switch to raw iptables management. Your existing firewall rules
|
||||
(which UFW manages via iptables under the hood) will be preserved and saved to
|
||||
<code>/etc/iptables/rules.v4</code>. iptables-persistent will be installed to ensure
|
||||
rules survive reboots.
|
||||
</p>
|
||||
<div style="margin-bottom:15px;padding:10px;border:1px solid #ffaa00;color:#ffaa00;">
|
||||
<strong>What happens:</strong><br>
|
||||
1. Current UFW/iptables state is backed up<br>
|
||||
2. UFW is disabled and its service stopped<br>
|
||||
3. The iptables rules that UFW generated are saved<br>
|
||||
4. iptables-persistent is installed for persistence<br>
|
||||
5. You manage rules directly via the iptables tab
|
||||
</div>
|
||||
<div id="m-ufw2ip-out" class="output" style="max-height:500px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="previewUfw2Ip()">Preview Current Rules</button>
|
||||
<button class="btn btn-danger" onclick="runUfw2Ip()">Migrate UFW → iptables</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ IPTABLES → UFW TAB ═══════════════════════ -->
|
||||
<div class="tab-content" id="panel-ip2ufw" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-title">Migrate: iptables → UFW</div>
|
||||
<p style="color:#888;margin-bottom:15px;">
|
||||
This will convert your iptables rules to UFW and enable UFW as the primary firewall frontend.
|
||||
Your current iptables rules are backed up before any changes. TCP/UDP ACCEPT rules on INPUT
|
||||
are automatically converted to UFW allow rules.
|
||||
</p>
|
||||
<div style="margin-bottom:15px;padding:10px;border:1px solid #ffaa00;color:#ffaa00;">
|
||||
<strong>What happens:</strong><br>
|
||||
1. Current iptables rules are backed up<br>
|
||||
2. UFW is installed (if not present) and reset<br>
|
||||
3. Default deny incoming / allow outgoing is set<br>
|
||||
4. iptables ACCEPT rules are converted to UFW allow rules<br>
|
||||
5. UFW is enabled, iptables-persistent is disabled<br>
|
||||
6. You manage rules via the UFW tab
|
||||
</div>
|
||||
<div id="m-ip2ufw-out" class="output" style="max-height:500px;margin-bottom:10px;"></div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="previewIp2Ufw()">Preview Current Rules</button>
|
||||
<button class="btn btn-danger" onclick="runIp2Ufw()">Migrate iptables → UFW</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ── Tab switching ──
|
||||
function showTab(name) {
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
|
||||
document.querySelectorAll('#tabs .btn').forEach(el => el.style.background = '');
|
||||
document.getElementById('panel-' + name).style.display = 'block';
|
||||
document.getElementById('tab-' + name).style.background = '#1a2a1a';
|
||||
}
|
||||
showTab('dashboard');
|
||||
|
||||
// ── Dashboard ──
|
||||
async function fwDetect() {
|
||||
const r = await apiGet('/api/firewall/detect');
|
||||
showResult(r, 'fw-detect');
|
||||
}
|
||||
async function fwPorts() {
|
||||
const r = await apiGet('/api/firewall/ports');
|
||||
showResult(r, 'fw-ports');
|
||||
}
|
||||
async function fwConns() {
|
||||
const r = await apiGet('/api/firewall/connections');
|
||||
showResult(r, 'fw-conns');
|
||||
}
|
||||
async function fwConnStats() {
|
||||
const r = await apiGet('/api/firewall/connection-stats');
|
||||
showResult(r, 'fw-connstats');
|
||||
}
|
||||
async function fwTopIPs() {
|
||||
const r = await apiGet('/api/firewall/top-ips');
|
||||
showResult(r, 'fw-topip');
|
||||
}
|
||||
async function fwBlocked() {
|
||||
const r = await apiGet('/api/firewall/blocked');
|
||||
showResult(r, 'fw-blocked');
|
||||
}
|
||||
async function fwLog() {
|
||||
const r = await apiGet('/api/firewall/log');
|
||||
showResult(r, 'fw-log');
|
||||
}
|
||||
|
||||
// ── UFW ──
|
||||
async function ufwStatus() {
|
||||
const r = await apiGet('/api/security/firewall/status');
|
||||
showResult(r, 'ufw-status');
|
||||
}
|
||||
async function ufwNumbered() {
|
||||
const r = await apiGet('/api/firewall/ufw/numbered');
|
||||
showResult(r, 'ufw-status');
|
||||
}
|
||||
async function ufwEnable() {
|
||||
const r = await apiPost('/api/security/firewall/enable');
|
||||
showResult(r, 'ufw-status');
|
||||
}
|
||||
async function ufwDisable() {
|
||||
if (!confirm('Disable UFW?')) return;
|
||||
const r = await apiPost('/api/security/firewall/disable');
|
||||
showResult(r, 'ufw-status');
|
||||
}
|
||||
async function ufwAdd() {
|
||||
const rule = document.getElementById('ufw-rule').value;
|
||||
const r = await apiPost('/api/security/firewall/add', {rule});
|
||||
showResult(r, 'ufw-status');
|
||||
}
|
||||
async function ufwDel() {
|
||||
const rule = document.getElementById('ufw-del-rule').value;
|
||||
const r = await apiPost('/api/security/firewall/delete', {rule});
|
||||
showResult(r, 'ufw-status');
|
||||
}
|
||||
async function ufwPreset(name) {
|
||||
if (!confirm('Apply ' + name + ' firewall preset?')) return;
|
||||
const r = await apiPost('/api/security/firewall/preset', {preset: name});
|
||||
showResult(r, 'ufw-status');
|
||||
}
|
||||
async function ufwDefault(policy, direction) {
|
||||
const r = await apiPost('/api/firewall/ufw/default', {policy, direction});
|
||||
showResult(r, 'ufw-defaults');
|
||||
}
|
||||
async function ufwAppList() {
|
||||
const r = await apiGet('/api/firewall/ufw/app-list');
|
||||
showResult(r, 'ufw-apps');
|
||||
}
|
||||
async function ufwLog() {
|
||||
const r = await apiGet('/api/firewall/ufw/log');
|
||||
showResult(r, 'ufw-log');
|
||||
}
|
||||
async function ufwLogLevel(level) {
|
||||
const r = await apiPost('/api/firewall/ufw/log-level', {level});
|
||||
showResult(r, 'ufw-log');
|
||||
}
|
||||
|
||||
// ── iptables ──
|
||||
async function iptList() {
|
||||
const r = await apiGet('/api/firewall/iptables/list');
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptListNat() {
|
||||
const r = await apiGet('/api/firewall/iptables/list-nat');
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptListMangle() {
|
||||
const r = await apiGet('/api/firewall/iptables/list-mangle');
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptCounters() {
|
||||
const r = await apiGet('/api/firewall/iptables/counters');
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptIp6() {
|
||||
const r = await apiGet('/api/firewall/iptables/ip6');
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptAdd() {
|
||||
const chain = document.getElementById('ipt-chain').value;
|
||||
const rule = document.getElementById('ipt-rule').value;
|
||||
const r = await apiPost('/api/firewall/iptables/add', {chain, rule});
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptInsert() {
|
||||
const chain = document.getElementById('ipt-chain').value;
|
||||
const rule = document.getElementById('ipt-rule').value;
|
||||
const pos = parseInt(document.getElementById('ipt-pos').value);
|
||||
const r = await apiPost('/api/firewall/iptables/insert', {chain, rule, position: pos});
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptDelete() {
|
||||
const chain = document.getElementById('ipt-del-chain').value;
|
||||
const rule_num = parseInt(document.getElementById('ipt-del-num').value);
|
||||
if (!confirm('Delete rule ' + rule_num + ' from ' + chain + '?')) return;
|
||||
const r = await apiPost('/api/firewall/iptables/delete', {chain, rule_num});
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptPolicy() {
|
||||
const chain = document.getElementById('ipt-pol-chain').value;
|
||||
const target = document.getElementById('ipt-pol-target').value;
|
||||
if (!confirm('Set ' + chain + ' policy to ' + target + '?')) return;
|
||||
const r = await apiPost('/api/firewall/iptables/policy', {chain, target});
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptBlockIP() {
|
||||
const ip = document.getElementById('ipt-ip').value;
|
||||
if (!ip) return;
|
||||
const r = await apiPost('/api/firewall/iptables/block-ip', {ip});
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptUnblockIP() {
|
||||
const ip = document.getElementById('ipt-ip').value;
|
||||
if (!ip) return;
|
||||
const r = await apiPost('/api/firewall/iptables/unblock-ip', {ip});
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptListBlocked() {
|
||||
const r = await apiGet('/api/firewall/iptables/blocked');
|
||||
showResult(r, 'ipt-rules');
|
||||
}
|
||||
async function iptSave() {
|
||||
const r = await apiPost('/api/firewall/iptables/save');
|
||||
showResult(r, 'ipt-persist');
|
||||
}
|
||||
async function iptRestore() {
|
||||
const r = await apiPost('/api/firewall/iptables/restore');
|
||||
showResult(r, 'ipt-persist');
|
||||
}
|
||||
async function iptFlush() {
|
||||
if (!confirm('Flush ALL iptables rules? This may lock you out if policy is DROP!')) return;
|
||||
const r = await apiPost('/api/firewall/iptables/flush');
|
||||
showResult(r, 'ipt-flush-out');
|
||||
}
|
||||
async function iptZero() {
|
||||
const r = await apiPost('/api/firewall/iptables/zero');
|
||||
showResult(r, 'ipt-flush-out');
|
||||
}
|
||||
async function iptLog() {
|
||||
const r = await apiGet('/api/firewall/iptables/log');
|
||||
showResult(r, 'ipt-log');
|
||||
}
|
||||
async function iptInstall() {
|
||||
const r = await apiPost('/api/firewall/iptables/install');
|
||||
showResult(r, 'ipt-install-out');
|
||||
}
|
||||
|
||||
// ── nftables ──
|
||||
async function nftList() {
|
||||
const r = await apiGet('/api/firewall/nftables/list');
|
||||
showResult(r, 'nft-rules');
|
||||
}
|
||||
async function nftTables() {
|
||||
const r = await apiGet('/api/firewall/nftables/tables');
|
||||
showResult(r, 'nft-rules');
|
||||
}
|
||||
async function nftCounters() {
|
||||
const r = await apiGet('/api/firewall/nftables/counters');
|
||||
showResult(r, 'nft-rules');
|
||||
}
|
||||
async function nftChains() {
|
||||
const table = document.getElementById('nft-table').value;
|
||||
const r = await apiPost('/api/firewall/nftables/chains', {table});
|
||||
showResult(r, 'nft-chains');
|
||||
}
|
||||
async function nftAddRule() {
|
||||
const table = document.getElementById('nft-add-table').value;
|
||||
const chain = document.getElementById('nft-add-chain').value;
|
||||
const rule = document.getElementById('nft-add-rule').value;
|
||||
const r = await apiPost('/api/firewall/nftables/add-rule', {table, chain, rule});
|
||||
showResult(r, 'nft-rules');
|
||||
}
|
||||
async function nftDelRule() {
|
||||
const table = document.getElementById('nft-del-table').value;
|
||||
const chain = document.getElementById('nft-del-chain').value;
|
||||
const handle = parseInt(document.getElementById('nft-del-handle').value);
|
||||
if (!confirm('Delete rule handle ' + handle + '?')) return;
|
||||
const r = await apiPost('/api/firewall/nftables/delete-rule', {table, chain, handle});
|
||||
showResult(r, 'nft-rules');
|
||||
}
|
||||
async function nftCreateTable() {
|
||||
const family = document.getElementById('nft-family').value;
|
||||
const name = document.getElementById('nft-new-table').value;
|
||||
const r = await apiPost('/api/firewall/nftables/create-table', {family, name});
|
||||
showResult(r, 'nft-rules');
|
||||
}
|
||||
async function nftCreateChain() {
|
||||
const table = document.getElementById('nft-family').value + ' ' + document.getElementById('nft-new-table').value;
|
||||
const chain = document.getElementById('nft-new-chain').value;
|
||||
const hook = document.getElementById('nft-hook').value;
|
||||
const r = await apiPost('/api/firewall/nftables/create-chain', {table, chain, hook});
|
||||
showResult(r, 'nft-rules');
|
||||
}
|
||||
async function nftSave() {
|
||||
const r = await apiPost('/api/firewall/nftables/save');
|
||||
showResult(r, 'nft-persist');
|
||||
}
|
||||
async function nftRestore() {
|
||||
const r = await apiPost('/api/firewall/nftables/restore');
|
||||
showResult(r, 'nft-persist');
|
||||
}
|
||||
async function nftConfig() {
|
||||
const r = await apiGet('/api/firewall/nftables/config');
|
||||
showResult(r, 'nft-persist');
|
||||
}
|
||||
async function nftFlush() {
|
||||
if (!confirm('Flush ALL nftables rules?')) return;
|
||||
const r = await apiPost('/api/firewall/nftables/flush');
|
||||
showResult(r, 'nft-flush-out');
|
||||
}
|
||||
async function nftInstall() {
|
||||
document.getElementById('nft-install-out').innerHTML = '<span class="info">Installing nftables...</span>';
|
||||
const r = await apiPost('/api/firewall/nftables/install');
|
||||
showResult(r, 'nft-install-out');
|
||||
}
|
||||
|
||||
// ── firewalld ──
|
||||
async function fwdStatus() {
|
||||
const r = await apiGet('/api/firewall/firewalld/status');
|
||||
showResult(r, 'fwd-status');
|
||||
}
|
||||
async function fwdReload() {
|
||||
const r = await apiPost('/api/firewall/firewalld/reload');
|
||||
showResult(r, 'fwd-status');
|
||||
}
|
||||
async function fwdPanicOn() {
|
||||
if (!confirm('Enable panic mode? ALL network traffic will be blocked!')) return;
|
||||
const r = await apiPost('/api/firewall/firewalld/panic-on');
|
||||
showResult(r, 'fwd-status');
|
||||
}
|
||||
async function fwdPanicOff() {
|
||||
const r = await apiPost('/api/firewall/firewalld/panic-off');
|
||||
showResult(r, 'fwd-status');
|
||||
}
|
||||
async function fwdZones() {
|
||||
const r = await apiGet('/api/firewall/firewalld/zones');
|
||||
showResult(r, 'fwd-zones');
|
||||
}
|
||||
async function fwdZoneInfo() {
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/zone-info', {zone});
|
||||
showResult(r, 'fwd-zones');
|
||||
}
|
||||
async function fwdSetDefault() {
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/default-zone', {zone});
|
||||
showResult(r, 'fwd-zones');
|
||||
}
|
||||
async function fwdServicesList() {
|
||||
const r = await apiGet('/api/firewall/firewalld/services-list');
|
||||
showResult(r, 'fwd-services');
|
||||
}
|
||||
async function fwdAddService() {
|
||||
const svc = document.getElementById('fwd-service').value;
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/add-service', {service: svc, zone});
|
||||
showResult(r, 'fwd-services');
|
||||
}
|
||||
async function fwdRemoveService() {
|
||||
const svc = document.getElementById('fwd-service').value;
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/remove-service', {service: svc, zone});
|
||||
showResult(r, 'fwd-services');
|
||||
}
|
||||
async function fwdAddPort() {
|
||||
const port = document.getElementById('fwd-port').value;
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/add-port', {port, zone});
|
||||
showResult(r, 'fwd-ports');
|
||||
}
|
||||
async function fwdRemovePort() {
|
||||
const port = document.getElementById('fwd-port').value;
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/remove-port', {port, zone});
|
||||
showResult(r, 'fwd-ports');
|
||||
}
|
||||
async function fwdAddRich() {
|
||||
const rule = document.getElementById('fwd-rich-rule').value;
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/add-rich-rule', {rule, zone});
|
||||
showResult(r, 'fwd-rich');
|
||||
}
|
||||
async function fwdRemoveRich() {
|
||||
const rule = document.getElementById('fwd-rich-rule').value;
|
||||
const zone = document.getElementById('fwd-zone').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/remove-rich-rule', {rule, zone});
|
||||
showResult(r, 'fwd-rich');
|
||||
}
|
||||
async function fwdBlockIP() {
|
||||
const ip = document.getElementById('fwd-ip').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/block-ip', {ip});
|
||||
showResult(r, 'fwd-block');
|
||||
}
|
||||
async function fwdUnblockIP() {
|
||||
const ip = document.getElementById('fwd-ip').value;
|
||||
const r = await apiPost('/api/firewall/firewalld/unblock-ip', {ip});
|
||||
showResult(r, 'fwd-block');
|
||||
}
|
||||
async function fwdLog() {
|
||||
const r = await apiGet('/api/firewall/firewalld/log');
|
||||
showResult(r, 'fwd-log');
|
||||
}
|
||||
async function fwdInstall() {
|
||||
document.getElementById('fwd-install-out').innerHTML = '<span class="info">Installing firewalld...</span>';
|
||||
const r = await apiPost('/api/firewall/firewalld/install');
|
||||
showResult(r, 'fwd-install-out');
|
||||
}
|
||||
|
||||
// ── CSF ──
|
||||
async function csfStatus() {
|
||||
const r = await apiGet('/api/firewall/csf/status');
|
||||
showResult(r, 'csf-status');
|
||||
}
|
||||
async function csfStart() {
|
||||
const r = await apiPost('/api/firewall/csf/start');
|
||||
showResult(r, 'csf-status');
|
||||
}
|
||||
async function csfStop() {
|
||||
if (!confirm('Stop CSF? This will flush all rules.')) return;
|
||||
const r = await apiPost('/api/firewall/csf/stop');
|
||||
showResult(r, 'csf-status');
|
||||
}
|
||||
async function csfRestart() {
|
||||
const r = await apiPost('/api/firewall/csf/restart');
|
||||
showResult(r, 'csf-status');
|
||||
}
|
||||
async function csfTest() {
|
||||
const r = await apiGet('/api/firewall/csf/test');
|
||||
showResult(r, 'csf-test');
|
||||
}
|
||||
async function csfList() {
|
||||
const r = await apiGet('/api/firewall/csf/list');
|
||||
showResult(r, 'csf-rules');
|
||||
}
|
||||
async function csfAllow() {
|
||||
const ip = document.getElementById('csf-ip').value;
|
||||
const comment = document.getElementById('csf-comment').value;
|
||||
const r = await apiPost('/api/firewall/csf/allow', {ip, comment});
|
||||
showResult(r, 'csf-ip-out');
|
||||
}
|
||||
async function csfDeny() {
|
||||
const ip = document.getElementById('csf-ip').value;
|
||||
const comment = document.getElementById('csf-comment').value;
|
||||
const r = await apiPost('/api/firewall/csf/deny', {ip, comment});
|
||||
showResult(r, 'csf-ip-out');
|
||||
}
|
||||
async function csfRemove() {
|
||||
const ip = document.getElementById('csf-ip').value;
|
||||
const r = await apiPost('/api/firewall/csf/remove', {ip});
|
||||
showResult(r, 'csf-ip-out');
|
||||
}
|
||||
async function csfGrep() {
|
||||
const ip = document.getElementById('csf-ip').value;
|
||||
const r = await apiPost('/api/firewall/csf/grep', {ip});
|
||||
showResult(r, 'csf-ip-out');
|
||||
}
|
||||
async function csfTempAllow() {
|
||||
const ip = document.getElementById('csf-ip').value;
|
||||
const ttl = parseInt(document.getElementById('csf-ttl').value);
|
||||
const r = await apiPost('/api/firewall/csf/temp-allow', {ip, ttl});
|
||||
showResult(r, 'csf-temp');
|
||||
}
|
||||
async function csfTempDeny() {
|
||||
const ip = document.getElementById('csf-ip').value;
|
||||
const ttl = parseInt(document.getElementById('csf-ttl').value);
|
||||
const r = await apiPost('/api/firewall/csf/temp-deny', {ip, ttl});
|
||||
showResult(r, 'csf-temp');
|
||||
}
|
||||
async function csfTempList() {
|
||||
const r = await apiGet('/api/firewall/csf/temp-list');
|
||||
showResult(r, 'csf-temp');
|
||||
}
|
||||
async function csfConfig() {
|
||||
const r = await apiGet('/api/firewall/csf/config');
|
||||
showResult(r, 'csf-config');
|
||||
}
|
||||
async function csfLog() {
|
||||
const r = await apiGet('/api/firewall/csf/log');
|
||||
showResult(r, 'csf-log');
|
||||
}
|
||||
async function csfInstall() {
|
||||
document.getElementById('csf-install-out').innerHTML = '<span class="info">Installing CSF...</span>';
|
||||
const r = await apiPost('/api/firewall/csf/install');
|
||||
showResult(r, 'csf-install-out');
|
||||
}
|
||||
|
||||
// ── Migration: UFW → iptables ──
|
||||
async function previewUfw2Ip() {
|
||||
const r = await apiGet('/api/security/firewall/status');
|
||||
showResult(r, 'm-ufw2ip-out');
|
||||
}
|
||||
async function runUfw2Ip() {
|
||||
if (!confirm('Migrate from UFW to raw iptables? UFW will be disabled. Make sure you have console access as backup!')) return;
|
||||
document.getElementById('m-ufw2ip-out').innerHTML = '<span class="info">Migrating UFW → iptables...</span>';
|
||||
const r = await apiPost('/api/firewall/migrate/ufw-to-iptables');
|
||||
showResult(r, 'm-ufw2ip-out');
|
||||
}
|
||||
|
||||
// ── Migration: iptables → UFW ──
|
||||
async function previewIp2Ufw() {
|
||||
const r = await apiGet('/api/firewall/iptables/list');
|
||||
showResult(r, 'm-ip2ufw-out');
|
||||
}
|
||||
async function runIp2Ufw() {
|
||||
if (!confirm('Migrate from iptables to UFW? Current rules will be converted. Make sure you have console access as backup!')) return;
|
||||
document.getElementById('m-ip2ufw-out').innerHTML = '<span class="info">Migrating iptables → UFW...</span>';
|
||||
const r = await apiPost('/api/firewall/migrate/iptables-to-ufw');
|
||||
showResult(r, 'm-ip2ufw-out');
|
||||
}
|
||||
|
||||
// Auto-load dashboard on page load
|
||||
fwDetect();
|
||||
</script>
|
||||
{% endblock %}
|
||||
182
setec-web/templates/frontpage.html
Normal file
182
setec-web/templates/frontpage.html
Normal file
@@ -0,0 +1,182 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Front Page Editor{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[<] Front Page Editor</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadFiles()">Refresh Files</button>
|
||||
<button class="btn" onclick="newFile()">+ New File</button>
|
||||
<button class="btn" onclick="openPreview()">Preview Site</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 15px; height: calc(100vh - 140px);">
|
||||
<!-- File list panel -->
|
||||
<div style="width: 220px; flex-shrink: 0;">
|
||||
<div class="card" style="height: 100%; overflow-y: auto;">
|
||||
<div class="card-title">Site Files</div>
|
||||
<div id="file-list" style="font-size: 12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor panel -->
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
||||
<span id="current-file" style="color: #888; font-size: 12px;">No file selected</span>
|
||||
<span id="modified-indicator" style="color: #ffaa00; font-size: 12px; display: none;">* modified</span>
|
||||
<div style="margin-left: auto;">
|
||||
<button class="btn" onclick="saveFile()" id="save-btn" disabled>Save</button>
|
||||
<button class="btn btn-danger" onclick="deleteFile()" id="delete-btn" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="editor" style="flex: 1; width: 100%; resize: none; font-size: 13px; line-height: 1.5; tab-size: 2;" disabled></textarea>
|
||||
<div id="output" class="output" style="max-height: 80px; margin-top: 8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentFile = null;
|
||||
let originalContent = '';
|
||||
const editor = document.getElementById('editor');
|
||||
const fileList = document.getElementById('file-list');
|
||||
const currentFileEl = document.getElementById('current-file');
|
||||
const modifiedEl = document.getElementById('modified-indicator');
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const deleteBtn = document.getElementById('delete-btn');
|
||||
|
||||
editor.addEventListener('input', () => {
|
||||
const modified = editor.value !== originalContent;
|
||||
modifiedEl.style.display = modified ? 'inline' : 'none';
|
||||
});
|
||||
|
||||
// Handle Ctrl+S to save
|
||||
editor.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
if (currentFile) saveFile();
|
||||
}
|
||||
// Tab inserts spaces
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = editor.selectionStart;
|
||||
const end = editor.selectionEnd;
|
||||
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
|
||||
editor.selectionStart = editor.selectionEnd = start + 2;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadFiles() {
|
||||
const res = await apiGet('/api/frontpage/list');
|
||||
if (!res.ok) {
|
||||
fileList.innerHTML = '<span class="err">Failed to load</span>';
|
||||
return;
|
||||
}
|
||||
const lines = (res.data.stdout || '').trim().split('\n').filter(l => l.trim());
|
||||
if (lines.length === 0) {
|
||||
fileList.innerHTML = '<span style="color:#888">No files found</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
lines.forEach(line => {
|
||||
const parts = line.split('|');
|
||||
const name = parts[0];
|
||||
const size = parts[1] ? formatSize(parseInt(parts[1])) : '';
|
||||
const ext = name.split('.').pop();
|
||||
const icon = ext === 'html' ? '</>' : ext === 'css' ? '#' : ext === 'js' ? 'js' : '?';
|
||||
const active = name === currentFile ? 'background:#1a2a1a;color:#fff;' : '';
|
||||
html += `<div onclick="openFile('${name}')" style="padding:6px 8px;cursor:pointer;border-bottom:1px solid #1a1a1a;${active}" onmouseover="this.style.background='#1a2a1a'" onmouseout="this.style.background='${name === currentFile ? '#1a2a1a' : ''}'">
|
||||
<span style="color:#555">[${icon}]</span> ${name}
|
||||
<span style="color:#555;font-size:10px;float:right">${size}</span>
|
||||
</div>`;
|
||||
});
|
||||
fileList.innerHTML = html;
|
||||
}
|
||||
|
||||
async function openFile(name) {
|
||||
if (currentFile && editor.value !== originalContent) {
|
||||
if (!confirm('Unsaved changes. Discard?')) return;
|
||||
}
|
||||
|
||||
const res = await apiGet('/api/frontpage/read?file=' + encodeURIComponent(name));
|
||||
if (!res.ok) {
|
||||
showResult(res);
|
||||
return;
|
||||
}
|
||||
currentFile = name;
|
||||
originalContent = res.data.stdout || '';
|
||||
editor.value = originalContent;
|
||||
editor.disabled = false;
|
||||
saveBtn.disabled = false;
|
||||
deleteBtn.disabled = false;
|
||||
currentFileEl.textContent = name;
|
||||
modifiedEl.style.display = 'none';
|
||||
document.getElementById('output').innerHTML = '<span class="info">Loaded ' + name + '</span>';
|
||||
loadFiles(); // refresh to highlight active
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
if (!currentFile) return;
|
||||
const res = await apiPost('/api/frontpage/write', {
|
||||
file: currentFile,
|
||||
content: editor.value
|
||||
});
|
||||
if (res.ok) {
|
||||
originalContent = editor.value;
|
||||
modifiedEl.style.display = 'none';
|
||||
document.getElementById('output').innerHTML = '<span class="status-ok">Saved ' + currentFile + '</span>';
|
||||
} else {
|
||||
showResult(res);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
if (!currentFile) return;
|
||||
if (!confirm('Delete ' + currentFile + '?')) return;
|
||||
const res = await api('/api/frontpage/delete', {
|
||||
method: 'DELETE',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({file: currentFile})
|
||||
});
|
||||
if (res.ok) {
|
||||
document.getElementById('output').innerHTML = '<span class="status-ok">Deleted ' + currentFile + '</span>';
|
||||
currentFile = null;
|
||||
editor.value = '';
|
||||
editor.disabled = true;
|
||||
saveBtn.disabled = true;
|
||||
deleteBtn.disabled = true;
|
||||
currentFileEl.textContent = 'No file selected';
|
||||
loadFiles();
|
||||
} else {
|
||||
showResult(res);
|
||||
}
|
||||
}
|
||||
|
||||
async function newFile() {
|
||||
const name = prompt('File name (e.g. newpage.html):');
|
||||
if (!name) return;
|
||||
const res = await apiPost('/api/frontpage/new', {file: name});
|
||||
if (res.ok) {
|
||||
await loadFiles();
|
||||
openFile(name);
|
||||
} else {
|
||||
showResult(res);
|
||||
}
|
||||
}
|
||||
|
||||
async function openPreview() {
|
||||
const res = await apiGet('/api/frontpage/preview-url');
|
||||
if (res.ok) {
|
||||
window.open(res.data, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + 'B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + 'K';
|
||||
return (bytes / 1048576).toFixed(1) + 'M';
|
||||
}
|
||||
|
||||
// Load on page open
|
||||
loadFiles();
|
||||
</script>
|
||||
{% endblock %}
|
||||
107
setec-web/templates/nginx.html
Normal file
107
setec-web/templates/nginx.html
Normal file
@@ -0,0 +1,107 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Nginx{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[>] Nginx / Subdomains</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadSites()">Refresh Sites</button>
|
||||
<button class="btn" onclick="reloadNginx()">Reload Nginx</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">Active Sites</div>
|
||||
<div class="output" id="sites-output"><span class="info">Loading...</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">Add Subdomain</div>
|
||||
<label>Subdomain (e.g. "api" for api.seteclabs.io)</label>
|
||||
<input type="text" id="sub-name" placeholder="subdomain" style="width:100%">
|
||||
<label>Type</label>
|
||||
<select id="sub-type" onchange="toggleSubFields()">
|
||||
<option value="proxy">Reverse Proxy</option>
|
||||
<option value="static">Static Files</option>
|
||||
</select>
|
||||
<div id="proxy-fields">
|
||||
<label>Proxy Port</label>
|
||||
<input type="number" id="sub-port" placeholder="e.g. 8080" style="width:100%">
|
||||
</div>
|
||||
<div id="static-fields" style="display:none">
|
||||
<label>Document Root</label>
|
||||
<input type="text" id="sub-root" placeholder="/var/www/sub.seteclabs.io" style="width:100%">
|
||||
</div>
|
||||
<br>
|
||||
<button class="btn" onclick="addSubdomain()">Create Vhost</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">SSL Certificate</div>
|
||||
<label>Domain</label>
|
||||
<input type="text" id="ssl-domain" placeholder="sub.seteclabs.io" style="width:100%">
|
||||
<br><br>
|
||||
<button class="btn" onclick="addSSL()">Install SSL (Certbot)</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">View Config</div>
|
||||
<label>Site name</label>
|
||||
<input type="text" id="config-site" placeholder="site filename" style="width:100%">
|
||||
<br><br>
|
||||
<button class="btn" onclick="viewConfig()">View</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output</div>
|
||||
<div class="output" id="output"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function toggleSubFields() {
|
||||
const t = document.getElementById('sub-type').value;
|
||||
document.getElementById('proxy-fields').style.display = t === 'proxy' ? '' : 'none';
|
||||
document.getElementById('static-fields').style.display = t === 'static' ? '' : 'none';
|
||||
}
|
||||
|
||||
async function loadSites() {
|
||||
const res = await apiGet('/api/nginx/sites');
|
||||
showResult(res, 'sites-output');
|
||||
}
|
||||
|
||||
async function reloadNginx() {
|
||||
const res = await apiPost('/api/nginx/reload');
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function addSubdomain() {
|
||||
const sub = document.getElementById('sub-name').value;
|
||||
const type = document.getElementById('sub-type').value;
|
||||
if (!sub) { alert('Enter subdomain'); return; }
|
||||
const body = {subdomain: sub};
|
||||
if (type === 'proxy') body.proxy_port = document.getElementById('sub-port').value;
|
||||
else body.static_root = document.getElementById('sub-root').value;
|
||||
const res = await apiPost('/api/nginx/add-subdomain', body);
|
||||
showResult(res);
|
||||
loadSites();
|
||||
}
|
||||
|
||||
async function addSSL() {
|
||||
const domain = document.getElementById('ssl-domain').value;
|
||||
if (!domain) return;
|
||||
document.getElementById('output').innerHTML = '<span class="info">Installing SSL for '+domain+'... (may take a minute)</span>';
|
||||
const res = await apiPost('/api/nginx/ssl/'+domain);
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function viewConfig() {
|
||||
const site = document.getElementById('config-site').value;
|
||||
if (!site) return;
|
||||
const res = await apiGet('/api/nginx/config/'+site);
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
loadSites();
|
||||
</script>
|
||||
{% endblock %}
|
||||
1598
setec-web/templates/security.html
Normal file
1598
setec-web/templates/security.html
Normal file
File diff suppressed because it is too large
Load Diff
216
setec-web/templates/settings.html
Normal file
216
setec-web/templates/settings.html
Normal file
@@ -0,0 +1,216 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[%] Settings</h1>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">VPS Connection</div>
|
||||
<label>Host</label>
|
||||
<input type="text" id="s-host" style="width:100%">
|
||||
<label>User</label>
|
||||
<input type="text" id="s-user" style="width:100%">
|
||||
<label>Port</label>
|
||||
<input type="number" id="s-port" style="width:100%">
|
||||
<label>SSH Key Path</label>
|
||||
<input type="text" id="s-key" style="width:100%">
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Domain & Paths</div>
|
||||
<label>Domain</label>
|
||||
<input type="text" id="s-domain" style="width:100%">
|
||||
<label>Web Root</label>
|
||||
<input type="text" id="s-webroot" style="width:100%">
|
||||
<label>Compose Path</label>
|
||||
<input type="text" id="s-compose" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Hosting Provider API</div>
|
||||
<p style="color:#888;font-size:11px;margin-bottom:10px">Select your hosting provider and enter API credentials for DNS management.</p>
|
||||
<label>Provider</label>
|
||||
<select id="s-provider" style="width:100%" onchange="providerChanged()">
|
||||
<option value="">-- Select Provider --</option>
|
||||
</select>
|
||||
<div id="provider-notes" style="font-size:11px;color:#555;margin:5px 3px"></div>
|
||||
<label id="lbl-apikey">API Key</label>
|
||||
<input type="text" id="s-apikey" style="width:100%" placeholder="Enter API key">
|
||||
<div id="provider-docs" style="font-size:11px;margin:5px 3px"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">E2E SSH Encryption</div>
|
||||
<p style="color:#888;font-size:11px;margin-bottom:10px">
|
||||
Encrypts all SSH commands with AES-256-GCM before transport. Requires the setec-agent on the VPS.
|
||||
</p>
|
||||
<div id="e2e-status" style="margin-bottom:10px;font-size:12px;color:#555">Loading...</div>
|
||||
<div class="toolbar" style="margin-bottom:0">
|
||||
<button class="btn" id="btn-e2e-toggle" onclick="toggleE2E()">Enable E2E</button>
|
||||
<button class="btn" onclick="deployE2E()">Deploy Agent</button>
|
||||
<button class="btn" onclick="testE2E()">Test Tunnel</button>
|
||||
</div>
|
||||
<div class="output" id="e2e-output" style="margin-top:10px;display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="saveSettings()">Save Settings</button>
|
||||
<button class="btn" onclick="loadSettings()">Reload</button>
|
||||
<button class="btn" onclick="testConnection()">Test SSH Connection</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output</div>
|
||||
<div class="output" id="output"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let providers = [];
|
||||
|
||||
async function loadProviders() {
|
||||
const res = await apiGet('/api/hosting/providers');
|
||||
if (!res.ok) return;
|
||||
providers = res.data;
|
||||
const sel = document.getElementById('s-provider');
|
||||
providers.forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function providerChanged() {
|
||||
const id = document.getElementById('s-provider').value;
|
||||
const p = providers.find(x => x.id === id);
|
||||
if (p) {
|
||||
document.getElementById('lbl-apikey').textContent = p.api_key_label || 'API Key';
|
||||
document.getElementById('provider-notes').textContent = p.notes || '';
|
||||
document.getElementById('provider-docs').innerHTML = p.docs ? '<a href="' + p.docs + '" target="_blank">' + p.docs + '</a>' : '';
|
||||
} else {
|
||||
document.getElementById('lbl-apikey').textContent = 'API Key';
|
||||
document.getElementById('provider-notes').textContent = '';
|
||||
document.getElementById('provider-docs').innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const res = await apiGet('/api/settings');
|
||||
if (!res.ok) { showResult(res); return; }
|
||||
const d = res.data;
|
||||
document.getElementById('s-host').value = d.vps_host || '';
|
||||
document.getElementById('s-user').value = d.vps_user || '';
|
||||
document.getElementById('s-port').value = d.vps_port || 22;
|
||||
document.getElementById('s-key').value = d.ssh_key_path || '';
|
||||
document.getElementById('s-domain').value = d.domain || '';
|
||||
document.getElementById('s-apikey').value = d.hostinger_api_key || '';
|
||||
document.getElementById('s-webroot').value = d.web_root || '';
|
||||
document.getElementById('s-compose').value = d.compose_path || '';
|
||||
if (d.hosting_provider) {
|
||||
document.getElementById('s-provider').value = d.hosting_provider;
|
||||
providerChanged();
|
||||
}
|
||||
document.getElementById('output').innerHTML = '<span class="info">Settings loaded.</span>';
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const body = {
|
||||
vps_host: document.getElementById('s-host').value,
|
||||
vps_user: document.getElementById('s-user').value,
|
||||
vps_port: parseInt(document.getElementById('s-port').value),
|
||||
ssh_key_path: document.getElementById('s-key').value,
|
||||
domain: document.getElementById('s-domain').value,
|
||||
hostinger_api_key: document.getElementById('s-apikey').value,
|
||||
hosting_provider: document.getElementById('s-provider').value,
|
||||
web_root: document.getElementById('s-webroot').value,
|
||||
compose_path: document.getElementById('s-compose').value,
|
||||
};
|
||||
const res = await apiPost('/api/settings', body);
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
document.getElementById('output').innerHTML = '<span class="info">Testing SSH...</span>';
|
||||
const res = await apiGet('/api/ssh/test');
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
// ── E2E Tunnel ──
|
||||
async function loadE2EStatus() {
|
||||
const res = await apiGet('/api/e2e/status');
|
||||
const el = document.getElementById('e2e-status');
|
||||
const btn = document.getElementById('btn-e2e-toggle');
|
||||
if (!res.ok) { el.innerHTML = '<span class="error">Failed to load E2E status</span>'; return; }
|
||||
const d = res.data;
|
||||
const active = d.e2e_enabled && d.e2e_deployed;
|
||||
let html = 'Status: ';
|
||||
if (active) {
|
||||
html += '<span class="status-ok">ACTIVE</span>';
|
||||
btn.textContent = 'Disable E2E';
|
||||
} else if (d.e2e_deployed) {
|
||||
html += '<span class="status-warn">Deployed but disabled</span>';
|
||||
btn.textContent = 'Enable E2E';
|
||||
} else {
|
||||
html += '<span class="status-err">Not deployed</span>';
|
||||
btn.textContent = 'Enable E2E';
|
||||
}
|
||||
html += ' | Agent: <code>' + d.agent_path + '</code>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
async function toggleE2E() {
|
||||
const res = await apiGet('/api/e2e/status');
|
||||
if (!res.ok) return;
|
||||
const nowEnabled = res.data.e2e_enabled && res.data.e2e_deployed;
|
||||
const out = document.getElementById('e2e-output');
|
||||
out.style.display = 'block';
|
||||
out.innerHTML = '<span class="info">' + (nowEnabled ? 'Disabling' : 'Enabling') + ' E2E...</span>';
|
||||
const r = await apiPost('/api/e2e/toggle', {enabled: !nowEnabled});
|
||||
if (r.ok) {
|
||||
out.innerHTML = '<span class="status-ok">E2E ' + (!nowEnabled ? 'enabled' : 'disabled') + '</span>';
|
||||
} else {
|
||||
out.innerHTML = '<span class="error">' + (r.error || 'Failed') + '</span>';
|
||||
}
|
||||
loadE2EStatus();
|
||||
}
|
||||
|
||||
async function deployE2E() {
|
||||
const out = document.getElementById('e2e-output');
|
||||
out.style.display = 'block';
|
||||
out.innerHTML = '<span class="info">Deploying agent + tunnel key to VPS... this may take a moment.</span>';
|
||||
const r = await apiPost('/api/e2e/deploy', {});
|
||||
if (r.ok) {
|
||||
let html = '<span class="status-ok">Deployment complete!</span><br>';
|
||||
(r.data || []).forEach(s => {
|
||||
html += (s.ok ? '<span class="status-ok">[OK]</span>' : '<span class="status-err">[FAIL]</span>') + ' ' + s.step + '<br>';
|
||||
});
|
||||
out.innerHTML = html;
|
||||
} else {
|
||||
let html = '<span class="error">' + (r.error || 'Deploy failed') + '</span><br>';
|
||||
(r.data || []).forEach(s => {
|
||||
html += (s.ok ? '<span class="status-ok">[OK]</span>' : '<span class="status-err">[FAIL]</span>') + ' ' + s.step + '<br>';
|
||||
});
|
||||
out.innerHTML = html;
|
||||
}
|
||||
loadE2EStatus();
|
||||
}
|
||||
|
||||
async function testE2E() {
|
||||
const out = document.getElementById('e2e-output');
|
||||
out.style.display = 'block';
|
||||
out.innerHTML = '<span class="info">Testing E2E tunnel...</span>';
|
||||
const r = await apiPost('/api/e2e/test', {});
|
||||
if (r.ok) {
|
||||
out.innerHTML = '<span class="status-ok">E2E tunnel working!</span><br><pre>' + (r.data.output || '') + '</pre>';
|
||||
} else {
|
||||
out.innerHTML = '<span class="error">' + (r.error || 'Test failed') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
loadProviders();
|
||||
loadSettings();
|
||||
loadE2EStatus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
161
setec-web/templates/smtp.html
Normal file
161
setec-web/templates/smtp.html
Normal file
@@ -0,0 +1,161 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}SMTP{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[*] SMTP / Mail</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="loadSmtpStatus()">Status</button>
|
||||
<button class="btn" onclick="checkDNS()">Check DNS</button>
|
||||
<button class="btn" onclick="flushQueue()">Flush Queue</button>
|
||||
<button class="btn btn-warn" onclick="restartSmtp()">Restart Postfix</button>
|
||||
<button class="btn" onclick="showTab('status')">Status</button>
|
||||
<button class="btn" onclick="showTab('send')">Send Email</button>
|
||||
<button class="btn" onclick="showTab('mass')">Mass Email (BCC)</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Status -->
|
||||
<div id="tab-status">
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">SMTP Status</div>
|
||||
<div class="output" id="smtp-output"><span class="info">Loading...</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">DNS Records (SPF/DKIM/DMARC)</div>
|
||||
<div class="output" id="dns-output"><span class="info">Click "Check DNS"</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Quick Test</div>
|
||||
<input type="email" id="test-email" placeholder="recipient@example.com" style="width:300px">
|
||||
<button class="btn" onclick="sendTest()">Send Test</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Send Email -->
|
||||
<div id="tab-send" style="display:none">
|
||||
<div class="card">
|
||||
<div class="card-title">Compose Email</div>
|
||||
<label>From</label>
|
||||
<input type="text" id="send-from" value="noreply@seteclabs.io" style="width:100%">
|
||||
<label>To</label>
|
||||
<input type="text" id="send-to" placeholder="recipient@example.com" style="width:100%">
|
||||
<label>Subject</label>
|
||||
<input type="text" id="send-subject" placeholder="Email subject" style="width:100%">
|
||||
<label>Body</label>
|
||||
<textarea id="send-body" rows="10" style="width:100%" placeholder="Type your message here..."></textarea>
|
||||
<br><br>
|
||||
<button class="btn" onclick="sendEmail()">Send Email</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Mass Email (BCC) -->
|
||||
<div id="tab-mass" style="display:none">
|
||||
<div class="card">
|
||||
<div class="card-title">Mass Email (BCC Mode)</div>
|
||||
<p style="color:#888;font-size:12px;margin-bottom:10px">
|
||||
Each recipient gets their own individual email. No one can see other recipients' addresses.
|
||||
</p>
|
||||
<label>From</label>
|
||||
<input type="text" id="mass-from" value="noreply@seteclabs.io" style="width:100%">
|
||||
<label>Recipients (comma or newline separated)</label>
|
||||
<textarea id="mass-to" rows="5" style="width:100%" placeholder="joe@example.com, sam@example.com, bob@example.com
|
||||
or one per line:
|
||||
joe@example.com
|
||||
sam@example.com
|
||||
bob@example.com"></textarea>
|
||||
<label>Subject</label>
|
||||
<input type="text" id="mass-subject" placeholder="Email subject" style="width:100%">
|
||||
<label>Body</label>
|
||||
<textarea id="mass-body" rows="10" style="width:100%" placeholder="Type your message here..."></textarea>
|
||||
<br><br>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="previewMass()">Preview (count recipients)</button>
|
||||
<button class="btn btn-warn" onclick="sendMass()">Send to All</button>
|
||||
</div>
|
||||
<div id="mass-preview" style="margin-top:10px;font-size:12px;color:#888"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Output</div>
|
||||
<div class="output" id="output"><span class="info">Ready.</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function showTab(tab) {
|
||||
document.getElementById('tab-status').style.display = tab === 'status' ? '' : 'none';
|
||||
document.getElementById('tab-send').style.display = tab === 'send' ? '' : 'none';
|
||||
document.getElementById('tab-mass').style.display = tab === 'mass' ? '' : 'none';
|
||||
}
|
||||
|
||||
// ── Status ──
|
||||
async function loadSmtpStatus() {
|
||||
const res = await apiGet('/api/smtp/status');
|
||||
showResult(res, 'smtp-output');
|
||||
}
|
||||
async function checkDNS() {
|
||||
const res = await apiGet('/api/smtp/dns-check');
|
||||
showResult(res, 'dns-output');
|
||||
}
|
||||
async function flushQueue() {
|
||||
const res = await apiPost('/api/smtp/flush');
|
||||
showResult(res);
|
||||
}
|
||||
async function restartSmtp() {
|
||||
const res = await apiPost('/api/smtp/restart');
|
||||
showResult(res);
|
||||
}
|
||||
async function sendTest() {
|
||||
const to = document.getElementById('test-email').value;
|
||||
if (!to) { alert('Enter email address'); return; }
|
||||
const res = await apiPost('/api/smtp/send-test', {to});
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
// ── Send Email ──
|
||||
async function sendEmail() {
|
||||
const from = document.getElementById('send-from').value.trim();
|
||||
const to = document.getElementById('send-to').value.trim();
|
||||
const subject = document.getElementById('send-subject').value.trim();
|
||||
const body = document.getElementById('send-body').value;
|
||||
if (!to || !subject) { alert('Fill in To and Subject'); return; }
|
||||
const res = await apiPost('/api/smtp/send', {from, to, subject, body});
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
// ── Mass Email ──
|
||||
function parseRecipients() {
|
||||
const raw = document.getElementById('mass-to').value;
|
||||
return raw.split(/[,\n]+/).map(e => e.trim()).filter(e => e && e.includes('@'));
|
||||
}
|
||||
|
||||
function previewMass() {
|
||||
const recipients = parseRecipients();
|
||||
const el = document.getElementById('mass-preview');
|
||||
el.innerHTML = `<span style="color:#00ff41">${recipients.length} recipients found:</span><br>` +
|
||||
recipients.map(r => ` - ${escHtml(r)}`).join('<br>');
|
||||
}
|
||||
|
||||
async function sendMass() {
|
||||
const recipients = parseRecipients();
|
||||
if (!recipients.length) { alert('Enter at least one recipient'); return; }
|
||||
const from = document.getElementById('mass-from').value.trim();
|
||||
const subject = document.getElementById('mass-subject').value.trim();
|
||||
const body = document.getElementById('mass-body').value;
|
||||
if (!subject) { alert('Enter a subject'); return; }
|
||||
if (!confirm(`Send email to ${recipients.length} recipients individually (BCC mode)?`)) return;
|
||||
|
||||
const out = document.getElementById('output');
|
||||
out.innerHTML = `<span class="info">Sending to ${recipients.length} recipients...</span>\n`;
|
||||
|
||||
const res = await apiPost('/api/smtp/send-mass', {from, recipients, subject, body});
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
loadSmtpStatus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
83
setec-web/templates/terminal.html
Normal file
83
setec-web/templates/terminal.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Terminal{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[$] SSH Terminal</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Remote Shell (root@VPS)</div>
|
||||
<div class="output" id="output" style="min-height:400px;max-height:700px"></div>
|
||||
<div style="display:flex;margin-top:5px">
|
||||
<span style="color:#00ff41;padding:6px">$</span>
|
||||
<input type="text" id="cmd-input" placeholder="enter command..." style="flex:1"
|
||||
onkeydown="if(event.key==='Enter')runCmd()"
|
||||
autofocus>
|
||||
<button class="btn" onclick="runCmd()">Run</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Quick Commands</div>
|
||||
<div class="toolbar">
|
||||
<button class="btn" onclick="quickCmd('uptime')">uptime</button>
|
||||
<button class="btn" onclick="quickCmd('free -h')">memory</button>
|
||||
<button class="btn" onclick="quickCmd('df -h /')">disk</button>
|
||||
<button class="btn" onclick="quickCmd('docker ps')">docker ps</button>
|
||||
<button class="btn" onclick="quickCmd('systemctl status nginx')">nginx status</button>
|
||||
<button class="btn" onclick="quickCmd('ufw status')">firewall</button>
|
||||
<button class="btn" onclick="quickCmd('tail -20 /var/log/syslog')">syslog</button>
|
||||
<button class="btn" onclick="quickCmd('last -10')">last logins</button>
|
||||
<button class="btn" onclick="quickCmd('netstat -tlnp')">open ports</button>
|
||||
<button class="btn" onclick="quickCmd('gitlab-ctl status')">gitlab status</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const history = [];
|
||||
let histIdx = -1;
|
||||
const cmdInput = document.getElementById('cmd-input');
|
||||
const output = document.getElementById('output');
|
||||
|
||||
cmdInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (histIdx < history.length - 1) { histIdx++; cmdInput.value = history[histIdx]; }
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (histIdx > 0) { histIdx--; cmdInput.value = history[histIdx]; }
|
||||
else { histIdx = -1; cmdInput.value = ''; }
|
||||
}
|
||||
});
|
||||
|
||||
async function runCmd() {
|
||||
const cmd = cmdInput.value.trim();
|
||||
if (!cmd) return;
|
||||
history.unshift(cmd);
|
||||
histIdx = -1;
|
||||
cmdInput.value = '';
|
||||
output.innerHTML += '<span style="color:#888">$ ' + escHtml(cmd) + '</span>\n';
|
||||
output.innerHTML += '<span class="info">running...</span>\n';
|
||||
output.scrollTop = output.scrollHeight;
|
||||
|
||||
const res = await apiPost('/api/terminal/exec', {cmd});
|
||||
// Remove the "running..." line
|
||||
output.innerHTML = output.innerHTML.replace(/<span class="info">running\.\.\.<\/span>\n$/, '');
|
||||
if (res.ok && res.data) {
|
||||
if (res.data.stdout) output.innerHTML += escHtml(res.data.stdout);
|
||||
if (res.data.stderr) output.innerHTML += '<span class="err">' + escHtml(res.data.stderr) + '</span>';
|
||||
if (res.data.exit_code && res.data.exit_code !== 0)
|
||||
output.innerHTML += '<span class="err">[exit: ' + res.data.exit_code + ']</span>\n';
|
||||
} else {
|
||||
output.innerHTML += '<span class="err">Error: ' + (res.error || 'unknown') + '</span>\n';
|
||||
}
|
||||
output.innerHTML += '\n';
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function quickCmd(cmd) {
|
||||
cmdInput.value = cmd;
|
||||
runCmd();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
516
setec-web/templates/wizard.html
Normal file
516
setec-web/templates/wizard.html
Normal file
@@ -0,0 +1,516 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Setup Wizard{% endblock %}
|
||||
{% block content %}
|
||||
<h1>[+] Setup Wizard</h1>
|
||||
|
||||
<div id="wiz-steps" style="margin-bottom:15px;font-size:11px;color:#555">
|
||||
<span id="ws-1" class="status-ok">[1] Terms</span> →
|
||||
<span id="ws-2">[2] SSH Keys</span> →
|
||||
<span id="ws-3">[3] VPS</span> →
|
||||
<span id="ws-4">[4] API</span> →
|
||||
<span id="ws-5">[5] Paths</span> →
|
||||
<span id="ws-6">[6] Test</span>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- Step 1: Terms / License -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="step-1" class="card">
|
||||
<div class="card-title">License Agreement & Terms of Use</div>
|
||||
<div style="font-size:12px;line-height:1.6;max-height:400px;overflow-y:auto;padding:10px;background:#000;border:1px solid #333;margin-bottom:15px">
|
||||
<p style="color:#ff4444;margin-bottom:10px"><strong>IMPORTANT - READ BEFORE CONTINUING</strong></p>
|
||||
|
||||
<p style="margin-bottom:10px"><strong style="color:#ff4444">1. RESTRICTED USE LICENSE</strong><br>
|
||||
This software is licensed for use by <strong>private individuals, independent security
|
||||
researchers, and non-governmental organizations ONLY</strong>. By using this software you
|
||||
affirm that you are not acting on behalf of, employed by, contracted by, or otherwise
|
||||
affiliated with any:<br>
|
||||
• <strong style="color:#ff4444">Law enforcement agency</strong> (local, state, federal, or international)<br>
|
||||
• <strong style="color:#ff4444">Government agency or department</strong> (civilian or military)<br>
|
||||
• <strong style="color:#ff4444">Intelligence service</strong> (domestic or foreign)<br>
|
||||
• <strong style="color:#ff4444">Government contractor</strong> performing work for any of the above<br>
|
||||
<br>
|
||||
Use of this software by any of the above entities or their agents is <strong>strictly
|
||||
prohibited</strong> and constitutes a violation of this license. This restriction applies
|
||||
regardless of the purpose, including but not limited to: investigations, surveillance,
|
||||
offensive operations, defensive operations, or "research." No exceptions.</p>
|
||||
|
||||
<p style="margin-bottom:10px"><strong style="color:#88ff88">2. NO WARRANTY</strong><br>
|
||||
This software is provided "AS IS" without warranty of any kind, express or implied.
|
||||
SETEC LABS makes no guarantees regarding reliability, availability, or fitness for
|
||||
any particular purpose. Use at your own risk.</p>
|
||||
|
||||
<p style="margin-bottom:10px"><strong style="color:#88ff88">3. NO GUARANTEE OF SUPPORT</strong><br>
|
||||
While the community may assist, there is no obligation to provide support, updates,
|
||||
or bug fixes. This is free, open-source software maintained by volunteers.</p>
|
||||
|
||||
<p style="margin-bottom:10px"><strong style="color:#88ff88">4. LIMITATION OF LIABILITY</strong><br>
|
||||
Under no circumstances shall SETEC LABS, its contributors, or the darkHal group be
|
||||
liable for any direct, indirect, incidental, or consequential damages arising from
|
||||
the use of this software. This includes but is not limited to: data loss, system
|
||||
downtime, security breaches, or misconfiguration of your server.</p>
|
||||
|
||||
<p style="margin-bottom:10px"><strong style="color:#ff4444">5. IF YOU PAID FOR THIS SOFTWARE, YOU WERE SCAMMED</strong><br>
|
||||
SETEC LABS Manager is <strong>100% free and open source</strong>. If someone charged you
|
||||
money for this application, you were ripped off and likely received a version bundled
|
||||
with malware. Delete it immediately.</p>
|
||||
|
||||
<p style="margin-bottom:10px"><strong style="color:#88ff88">6. OFFICIAL DOWNLOAD SOURCES</strong><br>
|
||||
Only download SETEC applications from these trusted sources:<br>
|
||||
• <span style="color:#00ff41">repo.seteclabs.io</span> (Official Gitea repository)<br>
|
||||
• <span style="color:#00ff41">github.com/DigiJEth</span> (Official GitHub mirror)<br>
|
||||
Any other source is unauthorized and potentially dangerous.</p>
|
||||
|
||||
<p style="margin-bottom:10px"><strong style="color:#88ff88">7. ROOT ACCESS WARNING</strong><br>
|
||||
This software executes commands on remote servers via SSH with the privileges of the
|
||||
configured user. Misconfiguration can result in data loss or security vulnerabilities.
|
||||
You are solely responsible for the actions performed through this tool.</p>
|
||||
|
||||
<p style="color:#888;font-size:10px;margin-top:15px;border-top:1px solid #333;padding-top:10px">
|
||||
SETEC LABS Manager • Free Software • darkHal Group • For the people, not the state.</p>
|
||||
</div>
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="display:inline;cursor:pointer">
|
||||
<input type="checkbox" id="tos-accept" onchange="tosChanged()" style="margin-right:8px">
|
||||
<span style="color:#ffaa00">I have read and accept these terms and I am not affiliated with any government or law enforcement entity</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn" id="btn-tos-next" onclick="acceptTOS()" disabled style="opacity:0.3">Next →</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- Step 2: SSH Key Selection/Generation -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="step-2" class="card" style="display:none">
|
||||
<div class="card-title">SSH Key Setup</div>
|
||||
|
||||
<p style="font-size:12px;color:#888;margin-bottom:15px">
|
||||
SETEC Manager connects to your VPS via SSH using key-based authentication.
|
||||
Do you already have SSH keys generated?
|
||||
</p>
|
||||
|
||||
<div style="margin-bottom:15px">
|
||||
<button class="btn" id="btn-has-keys" onclick="sshKeyChoice('yes')" style="padding:10px 20px">
|
||||
Yes, I have SSH keys
|
||||
</button>
|
||||
<button class="btn" id="btn-no-keys" onclick="sshKeyChoice('no')" style="padding:10px 20px">
|
||||
No, I need to create them
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- YES: User has keys -->
|
||||
<div id="ssh-has-keys" style="display:none">
|
||||
<label>SSH Private Key Path</label>
|
||||
<input type="text" id="w-key" style="width:100%" placeholder="C:/keys/setec">
|
||||
<div style="font-size:10px;color:#555;margin:2px 3px 5px">
|
||||
Enter the full path to your <strong style="color:#888">private</strong> key file (not the .pub file).
|
||||
</div>
|
||||
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
|
||||
Common locations:<br>
|
||||
<span style="color:#00ff41;line-height:1.8">
|
||||
• C:/Users/YourName/.ssh/id_ed25519<br>
|
||||
• C:/Users/YourName/.ssh/id_rsa<br>
|
||||
• C:/keys/setec<br>
|
||||
• ~/.ssh/id_ed25519 (Linux/Mac)
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top:10px">
|
||||
<button class="btn" onclick="goStep(1)">← Back</button>
|
||||
<button class="btn" onclick="saveKeyAndContinue()">Save & Continue →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NO: User needs keys -->
|
||||
<div id="ssh-no-keys" style="display:none">
|
||||
<p style="font-size:12px;color:#ffaa00;margin-bottom:10px">
|
||||
No problem! Select your VPS hosting provider below for a step-by-step guide.
|
||||
</p>
|
||||
|
||||
<label>Select your VPS host</label>
|
||||
<select id="w-ssh-host" style="width:100%" onchange="sshHostChanged()">
|
||||
<option value="">-- Select Host --</option>
|
||||
<option value="hostinger">Hostinger</option>
|
||||
<option value="digitalocean">DigitalOcean</option>
|
||||
<option value="vultr">Vultr</option>
|
||||
<option value="linode">Linode (Akamai)</option>
|
||||
<option value="hetzner">Hetzner</option>
|
||||
<option value="ovh">OVH / OVHcloud</option>
|
||||
<option value="aws">AWS (EC2)</option>
|
||||
<option value="contabo">Contabo</option>
|
||||
<option value="other">Other / Self-Hosted</option>
|
||||
</select>
|
||||
|
||||
<div id="ssh-host-guide" style="display:none;margin-top:15px;padding:12px;background:#000;border:1px solid #333;font-size:11px;line-height:1.8"></div>
|
||||
|
||||
<div id="ssh-generic-guide" style="display:none;margin-top:15px;padding:12px;background:#000;border:1px solid #333">
|
||||
<p style="color:#88ff88;font-size:12px;margin-bottom:8px"><strong>Generate SSH Keys (any platform)</strong></p>
|
||||
<p style="font-size:11px;color:#888;margin-bottom:8px">Open a terminal and run:</p>
|
||||
<div style="font-size:10px;margin-bottom:10px">
|
||||
<p style="color:#888;margin-bottom:3px">1. Generate key pair:</p>
|
||||
<code style="color:#00ff41">ssh-keygen -t ed25519 -f C:/keys/setec -N ""</code>
|
||||
</div>
|
||||
<div style="font-size:10px;margin-bottom:10px">
|
||||
<p style="color:#888;margin-bottom:3px">2. Copy the public key to your server:</p>
|
||||
<code style="color:#00ff41">ssh-copy-id -i C:/keys/setec.pub root@YOUR_SERVER_IP</code>
|
||||
</div>
|
||||
<div style="font-size:10px;margin-bottom:10px">
|
||||
<p style="color:#888;margin-bottom:3px">3. Test the connection:</p>
|
||||
<code style="color:#00ff41">ssh -i C:/keys/setec root@YOUR_SERVER_IP</code>
|
||||
</div>
|
||||
<p style="font-size:10px;color:#555;margin-top:8px">Your private key will be at <span style="color:#00ff41">C:/keys/setec</span></p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:15px">
|
||||
<label>Once your keys are ready, enter the private key path:</label>
|
||||
<input type="text" id="w-key-new" style="width:100%" placeholder="C:/keys/setec" value="C:/keys/setec">
|
||||
</div>
|
||||
|
||||
<div style="margin-top:10px">
|
||||
<button class="btn" onclick="goStep(1)">← Back</button>
|
||||
<button class="btn" onclick="saveNewKeyAndContinue()">Save & Continue →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- Step 3: VPS Connection -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="step-3" class="card" style="display:none">
|
||||
<div class="card-title">VPS Connection Setup</div>
|
||||
|
||||
<label>Server IP Address</label>
|
||||
<input type="text" id="w-host" style="width:100%" placeholder="e.g. 192.168.1.100">
|
||||
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
|
||||
Find your VPS IP in your hosting provider's control panel.
|
||||
</div>
|
||||
|
||||
<label>SSH Username</label>
|
||||
<input type="text" id="w-user" style="width:100%" placeholder="root" value="root">
|
||||
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
|
||||
<strong style="color:#888">Recommended:</strong> Use <span style="color:#00ff41">root</span> or create a sudo user:<br>
|
||||
<code style="color:#00ff41;font-size:10px">adduser setecadmin && usermod -aG sudo setecadmin</code><br>
|
||||
<span style="color:#ffaa00">Important:</span> Disable password login after setting up SSH keys:<br>
|
||||
<code style="color:#00ff41;font-size:10px">sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && systemctl restart sshd</code>
|
||||
</div>
|
||||
|
||||
<label>SSH Port</label>
|
||||
<input type="number" id="w-port" style="width:100%" placeholder="2222" value="2222">
|
||||
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
|
||||
<strong style="color:#ffaa00">We strongly recommend port 2222</strong> instead of default 22 to reduce brute-force attacks.<br>
|
||||
<code style="color:#00ff41;font-size:10px">sed -i 's/^#*Port .*/Port 2222/' /etc/ssh/sshd_config && ufw allow 2222/tcp && systemctl restart sshd</code><br>
|
||||
<span style="color:#ff4444">Do NOT close your current session until you verify the new port works!</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:10px">
|
||||
<button class="btn" onclick="goStep(2)">← Back</button>
|
||||
<button class="btn" onclick="saveVPS()">Save & Continue →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- Step 4: API Setup -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="step-4" class="card" style="display:none">
|
||||
<div class="card-title">DNS API Setup</div>
|
||||
|
||||
<label>Domain</label>
|
||||
<input type="text" id="w-domain" style="width:100%" placeholder="example.com">
|
||||
<div style="font-size:10px;color:#555;margin:2px 3px 5px">Your primary domain name managed by this panel.</div>
|
||||
|
||||
<label>DNS Provider</label>
|
||||
<select id="w-provider" style="width:100%" onchange="wizProviderChanged()">
|
||||
<option value="">-- Select Provider --</option>
|
||||
</select>
|
||||
<div id="w-provider-notes" style="font-size:11px;color:#ffaa00;margin:5px 3px;min-height:20px"></div>
|
||||
|
||||
<label id="w-lbl-apikey">API Key</label>
|
||||
<input type="text" id="w-apikey" style="width:100%" placeholder="Enter API key">
|
||||
<div id="w-provider-docs" style="font-size:11px;margin:5px 3px"></div>
|
||||
<div id="w-provider-help" style="font-size:10px;color:#555;margin:2px 3px 10px;display:none">
|
||||
<div id="w-help-content"></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:10px">
|
||||
<button class="btn" onclick="goStep(3)">← Back</button>
|
||||
<button class="btn" onclick="saveAPI()">Save & Continue →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- Step 5: Paths -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div id="step-5" class="card" style="display:none">
|
||||
<div class="card-title">Web Root & Compose Path</div>
|
||||
|
||||
<label>Web Root Directory</label>
|
||||
<input type="text" id="w-webroot" style="width:100%" placeholder="/var/www" value="/var/www">
|
||||
<div style="font-size:10px;color:#555;margin:2px 3px 10px">Default: <span style="color:#00ff41">/var/www</span></div>
|
||||
|
||||
<label>Docker Compose Path</label>
|
||||
<input type="text" id="w-compose" style="width:100%" placeholder="/opt/seteclabs/docker-compose.yml">
|
||||
<div style="font-size:10px;color:#00ff41;margin:0 3px 5px;line-height:1.8">
|
||||
• /opt/seteclabs/docker-compose.yml<br>
|
||||
• /root/docker-compose.yml<br>
|
||||
• /srv/docker-compose.yml
|
||||
</div>
|
||||
|
||||
<div style="margin-top:10px">
|
||||
<button class="btn" onclick="goStep(4)">← Back</button>
|
||||
<button class="btn" onclick="savePaths()">Save & Finish Setup →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 6: Test (modal trigger) -->
|
||||
<div id="step-6" style="display:none"></div>
|
||||
|
||||
<!-- Modal overlay -->
|
||||
<div id="wiz-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:999;display:none;align-items:center;justify-content:center">
|
||||
<div style="background:#111;border:1px solid #00ff41;padding:25px;max-width:500px;width:90%;max-height:80vh;overflow-y:auto">
|
||||
<div id="modal-title" style="font-size:14px;color:#88ff88;margin-bottom:15px;border-bottom:1px solid #333;padding-bottom:8px">Setup Complete</div>
|
||||
<div id="modal-body" style="font-size:12px;line-height:1.6"></div>
|
||||
<div id="modal-buttons" style="margin-top:15px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let wizProviders = [];
|
||||
let currentStep = 1;
|
||||
|
||||
// ── Step navigation ─────────────────────────────────────────────
|
||||
function goStep(n) {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
const el = document.getElementById('step-' + i);
|
||||
if (el) el.style.display = (i === n) ? 'block' : 'none';
|
||||
const ws = document.getElementById('ws-' + i);
|
||||
if (ws) ws.className = (i <= n) ? 'status-ok' : '';
|
||||
}
|
||||
currentStep = n;
|
||||
}
|
||||
|
||||
function tosChanged() {
|
||||
const btn = document.getElementById('btn-tos-next');
|
||||
if (document.getElementById('tos-accept').checked) {
|
||||
btn.disabled = false; btn.style.opacity = '1';
|
||||
} else {
|
||||
btn.disabled = true; btn.style.opacity = '0.3';
|
||||
}
|
||||
}
|
||||
|
||||
// ── TOS acceptance ──────────────────────────────────────────────
|
||||
async function acceptTOS() {
|
||||
const res = await fetch('/api/wizard/accept-tos', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({})
|
||||
}).then(r => r.json());
|
||||
if (res.ok) {
|
||||
goStep(2);
|
||||
} else {
|
||||
alert('Error saving TOS acceptance: ' + (res.error || 'Unknown'));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Provider help ────────────────────────────────────────────────
|
||||
const providerHelp = {
|
||||
hostinger: 'Log in to <strong>hPanel</strong> → click your profile icon → <strong>API Keys</strong> → Create new key with DNS permissions.',
|
||||
cloudflare: 'Go to <strong>dash.cloudflare.com</strong> → My Profile → <strong>API Tokens</strong> → Create Token → use "Edit zone DNS" template.',
|
||||
digitalocean: 'Go to <strong>cloud.digitalocean.com</strong> → API → <strong>Tokens</strong> → Generate New Token with read+write scope.',
|
||||
vultr: 'Go to <strong>my.vultr.com</strong> → Account → <strong>API</strong> → Enable API and copy the key.',
|
||||
linode: 'Go to <strong>cloud.linode.com</strong> → My Profile → <strong>API Tokens</strong> → Create Personal Access Token.',
|
||||
godaddy: 'Go to <strong>developer.godaddy.com</strong> → API Keys → Create New API Key. Format: <span style="color:#00ff41">key:secret</span>.',
|
||||
namecheap: 'Go to <strong>namecheap.com</strong> → Profile → Tools → <strong>API Access</strong>. Whitelist your IP.',
|
||||
hetzner: 'Go to <strong>dns.hetzner.com</strong> → API Tokens → Create new token.',
|
||||
ovh: 'Go to <strong>api.ovh.com/createApp</strong>. You need Application Key, Application Secret, and Consumer Key.',
|
||||
aws_route53: 'In <strong>AWS IAM Console</strong>, create access key with Route53 permissions. Format: <span style="color:#00ff41">ACCESS_KEY:SECRET</span>.'
|
||||
};
|
||||
|
||||
async function loadWizProviders() {
|
||||
const res = await apiGet('/api/hosting/providers');
|
||||
if (!res.ok) return;
|
||||
wizProviders = res.data;
|
||||
const sel = document.getElementById('w-provider');
|
||||
wizProviders.forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function wizProviderChanged() {
|
||||
const id = document.getElementById('w-provider').value;
|
||||
const p = wizProviders.find(x => x.id === id);
|
||||
if (p) {
|
||||
document.getElementById('w-lbl-apikey').textContent = p.api_key_label || 'API Key';
|
||||
document.getElementById('w-provider-notes').textContent = p.notes || '';
|
||||
document.getElementById('w-provider-docs').innerHTML = p.docs ?
|
||||
'Docs: <a href="' + escHtml(p.docs) + '" target="_blank" style="color:#00ff41">' + escHtml(p.docs) + '</a>' : '';
|
||||
const helpDiv = document.getElementById('w-provider-help');
|
||||
const helpContent = document.getElementById('w-help-content');
|
||||
if (providerHelp[id]) {
|
||||
helpContent.innerHTML = '<strong style="color:#88ff88">How to get your key:</strong><br>' + providerHelp[id];
|
||||
helpDiv.style.display = 'block';
|
||||
} else { helpDiv.style.display = 'none'; }
|
||||
} else {
|
||||
document.getElementById('w-lbl-apikey').textContent = 'API Key';
|
||||
document.getElementById('w-provider-notes').textContent = '';
|
||||
document.getElementById('w-provider-docs').innerHTML = '';
|
||||
document.getElementById('w-provider-help').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSH host guides ─────────────────────────────────────────────
|
||||
const sshHostGuides = {
|
||||
hostinger: { name: 'Hostinger', url: 'https://support.hostinger.com/en/articles/1583522-how-to-generate-ssh-keys', steps: 'Log in to <strong>hPanel</strong> → VPS → Settings → <strong>SSH Keys</strong> → Add SSH Key.' },
|
||||
digitalocean: { name: 'DigitalOcean', url: 'https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/', steps: 'Go to <strong>Settings</strong> → <strong>Security</strong> → SSH Keys → Add SSH Key.' },
|
||||
vultr: { name: 'Vultr', url: 'https://docs.vultr.com/how-do-i-generate-ssh-keys', steps: 'Go to <strong>Account</strong> → <strong>SSH Keys</strong> → Add SSH Key.' },
|
||||
linode: { name: 'Linode (Akamai)', url: 'https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/', steps: 'Go to <strong>Profile</strong> → <strong>SSH Keys</strong> → Add SSH Key.' },
|
||||
hetzner: { name: 'Hetzner', url: 'https://docs.hetzner.com/cloud/servers/getting-started/connecting-to-the-server/', steps: 'Go to <strong>Security</strong> → <strong>SSH Keys</strong> in Hetzner Cloud Console.' },
|
||||
ovh: { name: 'OVH', url: 'https://help.ovhcloud.com/csm/en-dedicated-servers-creating-ssh-keys?id=kb_article_view&sysparm_article=KB0047697', steps: 'In OVHcloud Control Panel → <strong>Public Cloud</strong> → <strong>SSH Keys</strong>.' },
|
||||
aws: { name: 'AWS (EC2)', url: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html', steps: 'AWS Console → EC2 → <strong>Key Pairs</strong> → Create or Import.' },
|
||||
contabo: { name: 'Contabo', url: 'https://contabo.com/blog/establishing-connection-server-ssh/', steps: 'Generate keys locally, copy manually with <code style="color:#00ff41">ssh-copy-id</code>.' },
|
||||
other: { name: 'Other', url: '', steps: '' }
|
||||
};
|
||||
|
||||
function sshKeyChoice(choice) {
|
||||
document.getElementById('ssh-has-keys').style.display = (choice === 'yes') ? 'block' : 'none';
|
||||
document.getElementById('ssh-no-keys').style.display = (choice === 'no') ? 'block' : 'none';
|
||||
document.getElementById('btn-has-keys').style.background = (choice === 'yes') ? '#00ff41' : '';
|
||||
document.getElementById('btn-has-keys').style.color = (choice === 'yes') ? '#000' : '';
|
||||
document.getElementById('btn-no-keys').style.background = (choice === 'no') ? '#00ff41' : '';
|
||||
document.getElementById('btn-no-keys').style.color = (choice === 'no') ? '#000' : '';
|
||||
}
|
||||
|
||||
function sshHostChanged() {
|
||||
const id = document.getElementById('w-ssh-host').value;
|
||||
const guideDiv = document.getElementById('ssh-host-guide');
|
||||
const genericDiv = document.getElementById('ssh-generic-guide');
|
||||
if (!id) { guideDiv.style.display = 'none'; genericDiv.style.display = 'none'; return; }
|
||||
const host = sshHostGuides[id];
|
||||
genericDiv.style.display = 'block';
|
||||
if (host && (host.url || host.steps)) {
|
||||
let html = '<p style="color:#88ff88;font-size:12px;margin-bottom:8px"><strong>' + escHtml(host.name) + ' SSH Key Guide</strong></p>';
|
||||
if (host.steps) html += '<p style="font-size:11px;color:#888;margin-bottom:8px">' + host.steps + '</p>';
|
||||
if (host.url) html += '<p style="margin-top:8px"><a href="' + escHtml(host.url) + '" target="_blank" style="color:#00ff41">' + escHtml(host.url) + '</a></p>';
|
||||
guideDiv.innerHTML = html;
|
||||
guideDiv.style.display = 'block';
|
||||
} else { guideDiv.style.display = 'none'; }
|
||||
}
|
||||
|
||||
function saveKeyAndContinue() {
|
||||
if (!document.getElementById('w-key').value) { alert('Enter SSH private key path.'); return; }
|
||||
goStep(3);
|
||||
}
|
||||
|
||||
function saveNewKeyAndContinue() {
|
||||
const k = document.getElementById('w-key-new').value;
|
||||
if (!k) { alert('Enter SSH private key path.'); return; }
|
||||
document.getElementById('w-key').value = k;
|
||||
goStep(3);
|
||||
}
|
||||
|
||||
function getKeyPath() {
|
||||
return document.getElementById('w-key').value || document.getElementById('w-key-new').value || '';
|
||||
}
|
||||
|
||||
async function saveVPS() {
|
||||
const body = { vps_host: document.getElementById('w-host').value, vps_user: document.getElementById('w-user').value,
|
||||
vps_port: parseInt(document.getElementById('w-port').value) || 22, ssh_key_path: getKeyPath() };
|
||||
if (!body.vps_host) { alert('Enter server IP.'); return; }
|
||||
if (!body.ssh_key_path) { alert('Go back and enter SSH key path.'); return; }
|
||||
const res = await apiPost('/api/settings', body);
|
||||
if (res.ok) goStep(4); else alert('Error: ' + (res.error || 'Unknown'));
|
||||
}
|
||||
|
||||
async function saveAPI() {
|
||||
const body = { domain: document.getElementById('w-domain').value, hosting_provider: document.getElementById('w-provider').value,
|
||||
hostinger_api_key: document.getElementById('w-apikey').value };
|
||||
if (!body.domain) { alert('Enter your domain.'); return; }
|
||||
const res = await apiPost('/api/settings', body);
|
||||
if (res.ok) goStep(5); else alert('Error: ' + (res.error || 'Unknown'));
|
||||
}
|
||||
|
||||
async function savePaths() {
|
||||
const body = { web_root: document.getElementById('w-webroot').value || '/var/www',
|
||||
compose_path: document.getElementById('w-compose').value, setup_complete: true };
|
||||
const res = await apiPost('/api/settings', body);
|
||||
if (res.ok) showTestModal(); else alert('Error: ' + (res.error || 'Unknown'));
|
||||
}
|
||||
|
||||
// ── Test modal (same as before) ─────────────────────────────────
|
||||
function showTestModal() {
|
||||
document.getElementById('wiz-modal').style.display = 'flex';
|
||||
document.getElementById('modal-title').textContent = 'Setup Complete!';
|
||||
document.getElementById('modal-body').innerHTML =
|
||||
'<p style="margin-bottom:10px">Your settings have been saved.</p>' +
|
||||
'<p>Would you like to test the connection?</p>';
|
||||
document.getElementById('modal-buttons').innerHTML =
|
||||
'<button class="btn" onclick="runTest()">Yes, Test Connection</button> ' +
|
||||
'<button class="btn" onclick="closeModal()">Skip</button>';
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
document.getElementById('modal-title').textContent = 'Testing Connection...';
|
||||
document.getElementById('modal-body').innerHTML = '<span style="color:#00ff41">Connecting via SSH...</span>';
|
||||
document.getElementById('modal-buttons').innerHTML = '';
|
||||
const sshRes = await apiGet('/api/wizard/test');
|
||||
if (sshRes.ok && sshRes.data) {
|
||||
const d = sshRes.data;
|
||||
if (d.ssh_ok) {
|
||||
let html = '<p style="color:#00ff41;margin-bottom:10px"><strong>SSH: SUCCESS</strong></p>';
|
||||
html += '<div style="background:#000;padding:8px;border:1px solid #333;font-size:11px;margin-bottom:10px">' + escHtml(d.ssh_output || '') + '</div>';
|
||||
if (d.api_ok) html += '<p style="color:#00ff41"><strong>DNS API: SUCCESS</strong></p>';
|
||||
else if (d.api_error) html += '<p style="color:#ffaa00"><strong>DNS API: ' + escHtml(d.api_error) + '</strong></p>';
|
||||
document.getElementById('modal-title').textContent = 'Connection Successful!';
|
||||
document.getElementById('modal-body').innerHTML = html;
|
||||
document.getElementById('modal-buttons').innerHTML =
|
||||
'<button class="btn" onclick="window.location.href=\'/\'">Go to Dashboard</button>';
|
||||
} else { showTestFailed(d.ssh_error || d.error || 'Connection failed'); }
|
||||
} else { showTestFailed(sshRes.error || 'Connection failed'); }
|
||||
}
|
||||
|
||||
function showTestFailed(error) {
|
||||
document.getElementById('modal-title').textContent = 'Connection Failed';
|
||||
document.getElementById('modal-body').innerHTML =
|
||||
'<p style="color:#ff4444;margin-bottom:10px"><strong>Connection Failed</strong></p>' +
|
||||
'<div style="background:#000;padding:8px;border:1px solid #ff4444;font-size:11px;margin-bottom:10px;color:#ff4444;white-space:pre-wrap">' + escHtml(error) + '</div>' +
|
||||
'<p style="color:#ffaa00;margin-bottom:8px">Check:</p>' +
|
||||
'<ul style="font-size:11px;color:#888;margin-left:15px;line-height:1.8">' +
|
||||
'<li>Server IP is correct and VPS is running</li>' +
|
||||
'<li>SSH port is open and correct</li>' +
|
||||
'<li>SSH key exists at the specified path</li>' +
|
||||
'<li>Public key is in ~/.ssh/authorized_keys on server</li></ul>';
|
||||
document.getElementById('modal-buttons').innerHTML =
|
||||
'<button class="btn" onclick="closeModal()">Close & Fix Settings</button>';
|
||||
}
|
||||
|
||||
function closeModal() { document.getElementById('wiz-modal').style.display = 'none'; }
|
||||
|
||||
// ── Prefill ─────────────────────────────────────────────────────
|
||||
async function prefillWizard() {
|
||||
const res = await apiGet('/api/settings');
|
||||
if (!res.ok) return;
|
||||
const d = res.data;
|
||||
if (d.vps_host) document.getElementById('w-host').value = d.vps_host;
|
||||
if (d.vps_user) document.getElementById('w-user').value = d.vps_user;
|
||||
if (d.vps_port) document.getElementById('w-port').value = d.vps_port;
|
||||
if (d.ssh_key_path) {
|
||||
document.getElementById('w-key').value = d.ssh_key_path;
|
||||
document.getElementById('w-key-new').value = d.ssh_key_path;
|
||||
}
|
||||
if (d.domain) document.getElementById('w-domain').value = d.domain;
|
||||
if (d.hostinger_api_key) document.getElementById('w-apikey').value = d.hostinger_api_key;
|
||||
if (d.web_root) document.getElementById('w-webroot').value = d.web_root;
|
||||
if (d.compose_path) document.getElementById('w-compose').value = d.compose_path;
|
||||
if (d.hosting_provider) {
|
||||
document.getElementById('w-provider').value = d.hosting_provider;
|
||||
wizProviderChanged();
|
||||
}
|
||||
}
|
||||
|
||||
loadWizProviders().then(() => prefillWizard());
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user