Files
admin 02650e3202 v0.23.0 — CSRF protection on all browser-facing POST endpoints
Controller:
- internal/web/csrf.go (new): CsrfProtect middleware, csrfToken/csrfField helpers
- auth.go: per-session CSRF token (csrfToken field, csrfTokenForSession method)
- server.go: executeTemplate wrapper auto-injects CSRFField+CSRFToken
- main.go: wire CsrfProtect on all routes; bump to v0.23.0
- handlers.go, storage_handlers.go, handler_restore.go: executeTemplate
- All templates: CSRFField in forms, meta csrf-token, csrfHeaders() JS helper,
  fetch calls updated; sendBeacon→fetch+keepalive in storage_attach.html

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:38:56 +01:00

583 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "storage_attach"}}
{{template "layout_start" .}}
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/settings" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Meglévő meghajtó csatolása</h2>
</div>
</div>
<!-- Step 1: Scan -->
<div class="settings-card" id="wizard-scan">
<h3>1. Meghajtók keresése</h3>
<p class="settings-card-desc">Keresse meg a rendszerhez csatlakoztatott, meglévő fájlrendszerrel rendelkező meghajtókat.</p>
<button class="btn btn-primary" onclick="scanDisks()" id="scan-btn">🔍 Meghajtók keresése</button>
<div id="scan-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
<div id="scan-result" style="display:none;margin-top:1.5rem">
<div id="available-disks"></div>
<div id="system-disks-note" style="display:none;margin-top:1rem"></div>
</div>
</div>
<!-- Step 2: Browse -->
<div class="settings-card" id="wizard-browse" style="display:none">
<h3>2. Mappa kiválasztása</h3>
<p class="settings-card-desc">Válasszon ki egy mappát a meghajtón, amelyet a controller használni fog. Új mappát is létrehozhat.</p>
<div id="browse-info" class="settings-grid" style="margin-bottom:1rem">
<div class="settings-row">
<span class="settings-label">Partíció</span>
<span class="settings-value mono" id="browse-device"></span>
</div>
<div class="settings-row">
<span class="settings-label">Fájlrendszer</span>
<span class="settings-value mono" id="browse-fstype"></span>
</div>
</div>
<div id="browse-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
<div id="dir-browser" style="border:1px solid var(--border);border-radius:6px;padding:1rem;background:var(--card-bg);margin-bottom:1rem">
<div id="dir-breadcrumb" class="form-hint mono" style="margin-bottom:.75rem"></div>
<div id="dir-list" style="min-height:100px"></div>
</div>
<div style="display:flex;gap:.75rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1rem">
<div class="form-group" style="margin-bottom:0">
<label for="new-dir-name">Új mappa neve</label>
<input type="text" id="new-dir-name" class="form-control" placeholder="felhom_data"
pattern="[a-zA-Z0-9_]+" style="max-width:200px">
</div>
<button class="btn btn-outline" onclick="createDir()" id="mkdir-btn">📁 Mappa létrehozása</button>
</div>
<div id="selected-dir-info" class="alert alert-info" style="display:none;margin-bottom:1rem">
Kiválasztott mappa: <strong id="selected-dir-display" class="mono"></strong>
</div>
<div class="form-actions" style="gap:.75rem">
<button class="btn btn-primary" onclick="goToConfigure()" id="browse-next-btn" disabled>Tovább →</button>
<button class="btn btn-outline" onclick="cancelAttach()">Mégsem</button>
</div>
</div>
<!-- Step 3: Configure -->
<div class="settings-card" id="wizard-configure" style="display:none">
<h3>3. Konfiguráció</h3>
<p class="settings-card-desc">Adja meg a csatolás paramétereit.</p>
<form id="attach-form">
<div class="form-group">
<label>Kiválasztott partíció</label>
<span class="settings-value mono" id="config-device-display"></span>
</div>
<div class="form-group">
<label>Kiválasztott mappa</label>
<span class="settings-value mono" id="config-subpath-display"></span>
</div>
<div class="form-group">
<label for="mount-name">Csatlakoztatási név <span class="required">*</span></label>
<div class="form-inline">
<span class="mono" style="opacity:.6">/mnt/</span>
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1"
pattern="[a-zA-Z0-9_]+" required style="max-width:160px">
</div>
<span class="form-hint">Pl. hdd_1 → a mappa a /mnt/hdd_1 útvonalra kerül</span>
</div>
<div class="form-group">
<label for="storage-label">Megnevezés</label>
<input type="text" id="storage-label" class="form-control" placeholder="Külső HDD 1TB" maxlength="50">
</div>
<label class="toggle" style="margin-bottom:1.5rem">
<input type="checkbox" id="set-default" checked>
<span class="toggle-label">Beállítás alapértelmezett adattárolóként új telepítéseknél</span>
</label>
<div class="alert alert-info" style="margin-bottom:1.5rem">
<strong>️ Megjegyzés:</strong> A meghajtón lévő adatok <strong>NEM</strong> törlődnek.
A controller csak a kiválasztott mappában dolgozik.<br>
<strong>⚠️ A csatlakozási pont (/mnt/&lt;név&gt;) a meghajtó lecsatolásáig nem módosítható.</strong>
</div>
<div class="form-actions" style="gap:.75rem">
<button type="submit" class="btn btn-primary" id="attach-btn">Csatolás</button>
<button type="button" class="btn btn-outline" onclick="backToBrowse()">← Vissza</button>
</div>
<div id="attach-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</form>
</div>
<!-- Step 4: Progress -->
<div class="settings-card" id="wizard-progress" style="display:none">
<h3>4. Csatolás folyamatban...</h3>
<div class="disk-progress-steps" id="progress-steps">
<div class="disk-step" id="pstep-validating"><span class="disk-step-icon"></span> Ellenőrzés</div>
<div class="disk-step" id="pstep-mounting"><span class="disk-step-icon"></span> Csatlakoztatás</div>
<div class="disk-step" id="pstep-permissions"><span class="disk-step-icon"></span> Mappák és jogosultságok</div>
<div class="disk-step" id="pstep-done"><span class="disk-step-icon"></span> Regisztráció</div>
</div>
<div style="margin-top:1.5rem;display:flex;align-items:center;gap:1rem">
<div class="progress-bar-task" style="flex:1">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
<span id="progress-percent" style="font-size:0.9rem;color:var(--text-muted);font-family:'JetBrains Mono',monospace;white-space:nowrap">0%</span>
</div>
<div id="progress-msg" class="form-hint" style="margin-top:.75rem"></div>
<div id="progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</div>
<!-- Step 5: Done -->
<div class="settings-card" id="wizard-done" style="display:none">
<h3>✅ Meghajtó sikeresen csatolva!</h3>
<div id="done-info" class="settings-grid" style="margin-top:1rem">
<div class="settings-row">
<span class="settings-label">Útvonal</span>
<span class="settings-value mono" id="done-path"></span>
</div>
</div>
<a href="/settings" class="btn btn-primary" style="margin-top:1.5rem">← Vissza a Beállításokhoz</a>
</div>
<script>
var selectedDevice = null;
var selectedPartition = null;
var currentBrowsePath = '';
var rawMountPath = '';
var selectedSubPath = '';
var pollTimer = null;
// --- Step 1: Scan ---
function scanDisks() {
var btn = document.getElementById('scan-btn');
var errEl = document.getElementById('scan-error');
var resultEl = document.getElementById('scan-result');
btn.textContent = 'Keresés...';
btn.disabled = true;
errEl.style.display = 'none';
resultEl.style.display = 'none';
// Clean up any stale raw mounts from interrupted previous sessions first,
// so the device appears as available in the scan results.
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders()})
.catch(function(){}) // ignore cancel errors
.then(function() { return fetch('/api/storage/scan', {method:'POST', headers: csrfHeaders()}); })
.then(function(r){ return r.json(); })
.then(function(data) {
btn.textContent = '🔍 Meghajtók keresése';
btn.disabled = false;
if (!data.ok) {
errEl.textContent = data.error || 'Ismeretlen hiba';
errEl.style.display = 'block';
return;
}
renderScanResult(data);
resultEl.style.display = 'block';
})
.catch(function(e) {
btn.textContent = '🔍 Meghajtók keresése';
btn.disabled = false;
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function renderScanResult(data) {
var availEl = document.getElementById('available-disks');
var sysEl = document.getElementById('system-disks-note');
// Filter: only show disks that have at least one partition with a filesystem
var disksWithFS = [];
if (data.available) {
data.available.forEach(function(disk) {
if (disk.Partitions) {
var fsPartitions = disk.Partitions.filter(function(p) { return p.FSType && p.FSType !== ''; });
if (fsPartitions.length > 0) {
disksWithFS.push({disk: disk, partitions: fsPartitions});
}
}
});
}
if (disksWithFS.length === 0) {
availEl.innerHTML = '<div class="empty-state" style="padding:1rem">Nem található meglévő fájlrendszerrel rendelkező meghajtó.<br>' +
'<span class="form-hint">Ha üres meghajtót szeretne inicializálni, használja az <a href="/settings/storage/init">inicializálás varázslót</a>.</span></div>';
return;
}
var html = '<h4 style="margin-bottom:.75rem">Talált meghajtók csatolható partíciókkal:</h4>';
disksWithFS.forEach(function(item) {
var disk = item.disk;
html += '<div style="margin-bottom:1rem">';
html += '<div class="form-hint" style="margin-bottom:.5rem">' + disk.Path + ' — ' + (disk.Size || '?') +
(disk.Model ? ' — ' + disk.Model : '') + '</div>';
item.partitions.forEach(function(part) {
var info = part.FSType;
if (part.Label) info += ', címke: ' + part.Label;
if (part.UUID) info += ', UUID: ' + part.UUID.substring(0, 8) + '...';
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent;margin-bottom:.5rem" ' +
'onclick="selectPartition(this, \'' + part.Path + '\', \'' + (part.FSType || '') + '\', \'' + (part.Label || '') + '\')" ' +
'data-path="' + part.Path + '">' +
'<div class="storage-path-header"><div class="storage-path-info">' +
'<span class="storage-path-label">○ ' + part.Path + ' — ' + (part.Size || '?') + '</span>' +
'<span class="form-hint">' + info + '</span>' +
'</div></div></div>';
});
html += '</div>';
});
availEl.innerHTML = html;
if (data.system && data.system.length > 0) {
var sysNames = data.system.map(function(d){ return d.Path + ' (' + (d.Size||'?') + ')'; }).join(', ');
sysEl.innerHTML = '<span class="form-hint">A rendszermeghajtó(k) nem választhatók: ' + sysNames + '</span>';
sysEl.style.display = 'block';
}
}
function selectPartition(el, path, fsType, label) {
// Deselect all
document.querySelectorAll('[data-path]').forEach(function(d) {
d.style.border = '2px solid transparent';
var lbl = d.querySelector('.storage-path-label');
if (lbl) lbl.textContent = lbl.textContent.replace('● ', '○ ');
});
// Select this
el.style.border = '2px solid var(--accent-blue)';
var lbl = el.querySelector('.storage-path-label');
if (lbl) lbl.textContent = lbl.textContent.replace('○ ', '● ');
selectedDevice = path;
selectedPartition = {path: path, fsType: fsType, label: label};
// Mount raw and go to browse
mountRawAndBrowse(path, fsType);
}
// --- Step 2: Browse ---
function mountRawAndBrowse(devicePath, fsType) {
var errEl = document.getElementById('scan-error');
errEl.style.display = 'none';
fetch('/api/storage/attach/mount-raw', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({device_path: devicePath})
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
errEl.textContent = data.error || 'Raw mount sikertelen';
errEl.style.display = 'block';
return;
}
rawMountPath = data.raw_path;
// Show browse step
document.getElementById('browse-device').textContent = devicePath;
document.getElementById('browse-fstype').textContent = fsType;
document.getElementById('wizard-scan').style.display = 'none';
document.getElementById('wizard-browse').style.display = 'block';
document.getElementById('wizard-browse').scrollIntoView({behavior:'smooth'});
// Browse root
browseDirectory(rawMountPath);
})
.catch(function(e) {
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function browseDirectory(path) {
currentBrowsePath = path;
var errEl = document.getElementById('browse-error');
errEl.style.display = 'none';
// Update breadcrumb
var rel = path.replace(rawMountPath, '') || '/';
document.getElementById('dir-breadcrumb').textContent = 'Aktuális mappa: ' + rel;
fetch('/api/storage/attach/browse?path=' + encodeURIComponent(path))
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
errEl.textContent = data.error || 'Hiba a mappák listázásakor';
errEl.style.display = 'block';
return;
}
renderDirList(data.dirs || [], path);
})
.catch(function(e) {
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function renderDirList(dirs, basePath) {
var listEl = document.getElementById('dir-list');
var html = '';
// "Use this directory" option (select the current directory itself)
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent;margin-bottom:.5rem;padding:.5rem .75rem" ' +
'onclick="selectDir(this, \'' + escapeJS(basePath) + '\')" data-dirpath="' + escapeAttr(basePath) + '">' +
'<span class="storage-path-label">📂 . (ez a mappa)</span></div>';
// Parent directory (if not at root)
if (basePath !== rawMountPath) {
var parentPath = basePath.substring(0, basePath.lastIndexOf('/'));
if (parentPath.length < rawMountPath.length) parentPath = rawMountPath;
html += '<div style="padding:.3rem .75rem;cursor:pointer;opacity:.7" onclick="browseDirectory(\'' + escapeJS(parentPath) + '\')">' +
'📁 .. (szülő mappa)</div>';
}
if (dirs.length === 0) {
html += '<div class="form-hint" style="padding:.5rem .75rem">Üres mappa</div>';
} else {
dirs.forEach(function(dir) {
html += '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.25rem">';
// Clickable to navigate into
if (dir.has_children) {
html += '<div style="padding:.3rem .75rem;cursor:pointer;flex:1" onclick="browseDirectory(\'' + escapeJS(dir.path) + '\')">' +
'📁 ' + dir.name + ' →</div>';
} else {
html += '<div style="padding:.3rem .75rem;flex:1">📁 ' + dir.name + '</div>';
}
// Select button
html += '<button class="btn btn-xs btn-outline" onclick="selectDir(null, \'' + escapeJS(dir.path) + '\')">Kiválasztás</button>';
html += '</div>';
});
}
listEl.innerHTML = html;
}
function selectDir(el, path) {
selectedSubPath = path;
document.getElementById('selected-dir-display').textContent = path.replace(rawMountPath, '') || '/';
document.getElementById('selected-dir-info').style.display = 'block';
document.getElementById('browse-next-btn').disabled = false;
// Highlight selected
document.querySelectorAll('[data-dirpath]').forEach(function(d) {
d.style.border = '2px solid transparent';
});
if (el) {
el.style.border = '2px solid var(--accent-blue)';
}
// Pre-fill mount name from partition label if available
var mountInput = document.getElementById('mount-name');
if (!mountInput.value && selectedPartition && selectedPartition.label) {
mountInput.value = selectedPartition.label.replace(/[^a-zA-Z0-9_]/g, '_');
}
}
function createDir() {
var nameInput = document.getElementById('new-dir-name');
var name = nameInput.value.trim();
if (!name) return;
var errEl = document.getElementById('browse-error');
errEl.style.display = 'none';
// Client-side validation
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
errEl.textContent = 'A mappanéven csak betűk, számok és alávonás megengedett.';
errEl.style.display = 'block';
return;
}
if (name.length > 32) {
errEl.textContent = 'A mappanév legfeljebb 32 karakter lehet.';
errEl.style.display = 'block';
return;
}
fetch('/api/storage/attach/mkdir', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({path: currentBrowsePath, name: name})
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
errEl.textContent = data.error || 'Mappa létrehozása sikertelen';
errEl.style.display = 'block';
return;
}
nameInput.value = '';
// Auto-select the created directory
selectedSubPath = data.created_path;
document.getElementById('selected-dir-display').textContent = data.created_path.replace(rawMountPath, '');
document.getElementById('selected-dir-info').style.display = 'block';
document.getElementById('browse-next-btn').disabled = false;
// Refresh directory listing
browseDirectory(currentBrowsePath);
})
.catch(function(e) {
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function goToConfigure() {
if (!selectedSubPath) return;
document.getElementById('config-device-display').textContent = selectedDevice;
document.getElementById('config-subpath-display').textContent = selectedSubPath.replace(rawMountPath, '') || '/ (gyökérmappa)';
document.getElementById('wizard-browse').style.display = 'none';
document.getElementById('wizard-configure').style.display = 'block';
document.getElementById('wizard-configure').scrollIntoView({behavior:'smooth'});
}
function backToBrowse() {
document.getElementById('wizard-configure').style.display = 'none';
document.getElementById('wizard-browse').style.display = 'block';
}
function cancelAttach() {
// Cleanup raw mount
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders()}).catch(function(){});
window.location.href = '/settings';
}
// --- Step 3: Submit ---
document.getElementById('attach-form').addEventListener('submit', function(e) {
e.preventDefault();
var mountName = document.getElementById('mount-name').value.trim();
var label = document.getElementById('storage-label').value.trim();
var setDefault = document.getElementById('set-default').checked;
var errEl = document.getElementById('attach-error');
if (!mountName) {
errEl.textContent = 'A csatlakoztatási nevet meg kell adni.';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
document.getElementById('wizard-configure').style.display = 'none';
document.getElementById('wizard-progress').style.display = 'block';
document.getElementById('wizard-progress').scrollIntoView({behavior:'smooth'});
var body = {
device_path: selectedDevice,
mount_name: mountName,
sub_path: selectedSubPath,
label: label,
set_default: setDefault
};
fetch('/api/storage/attach', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify(body)
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
showProgressError(data.error || 'Ismeretlen hiba');
return;
}
pollTimer = setInterval(pollProgress, 1500);
})
.catch(function(e) {
showProgressError('Hálózati hiba: ' + e.message);
});
});
// --- Step 4: Progress ---
var stepOrder = ['validating','mounting','permissions','done'];
function pollProgress() {
fetch('/api/storage/attach/status')
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) return;
updateProgressUI(data);
if (data.done) {
clearInterval(pollTimer);
if (data.step === 'done') {
showDone('/mnt/' + document.getElementById('mount-name').value.trim());
}
}
})
.catch(function(){});
}
function updateProgressUI(data) {
var currentIdx = stepOrder.indexOf(data.step);
stepOrder.forEach(function(s, i) {
var el = document.getElementById('pstep-' + s);
if (!el) return;
var icon = el.querySelector('.disk-step-icon');
if (i < currentIdx) {
el.className = 'disk-step disk-step-done';
icon.textContent = '✅';
} else if (i === currentIdx) {
el.className = 'disk-step disk-step-active';
icon.textContent = data.step === 'error' ? '❌' : '⏳';
} else {
el.className = 'disk-step';
icon.textContent = '○';
}
});
var pct = data.pct || 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-percent').textContent = pct + '%';
document.getElementById('progress-msg').textContent = data.msg || '';
if (data.step === 'error' || data.error) {
showProgressError(data.error || data.msg || 'Ismeretlen hiba');
}
}
function showProgressError(msg) {
clearInterval(pollTimer);
document.getElementById('progress-error').textContent = 'Hiba: ' + msg;
document.getElementById('progress-error').style.display = 'block';
document.getElementById('wizard-progress').querySelector('h3').textContent = 'Csatolás sikertelen';
}
function showDone(mountPath) {
document.getElementById('wizard-progress').style.display = 'none';
document.getElementById('wizard-done').style.display = 'block';
document.getElementById('done-path').textContent = mountPath;
document.getElementById('wizard-done').scrollIntoView({behavior:'smooth'});
}
// --- Helpers ---
function escapeJS(s) {
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function escapeAttr(s) {
return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Cleanup on page unload (best-effort)
window.addEventListener('beforeunload', function() {
if (rawMountPath && !document.getElementById('wizard-done').style.display !== 'none') {
// Best-effort cleanup via fetch (sendBeacon can't send CSRF headers)
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders(), keepalive: true}).catch(function(){});
}
});
</script>
{{template "layout_end" .}}
{{end}}